feat(skills): enhance skills system with matching and evaluation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
153
backend/app/agents/skills/retriever.py
Normal file
153
backend/app/agents/skills/retriever.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from app.agents.schemas.skills import SkillShortlistEntry
|
||||
from app.agents.skills.matcher import score_text_match
|
||||
from app.agents.skills.policy import choose_injection_mode, render_skill_shortlist_context
|
||||
from app.agents.skills.registry import get_skill_registry
|
||||
from app.services.skill_service import SkillService
|
||||
|
||||
|
||||
class RuntimeSkillRetriever:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def shortlist(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
query_text: str,
|
||||
memory_context: str | None = None,
|
||||
retrospectives: list[dict] | None = None,
|
||||
include_learned: bool = True,
|
||||
limit: int = 3,
|
||||
) -> list[SkillShortlistEntry]:
|
||||
deduped: "OrderedDict[str, SkillShortlistEntry]" = OrderedDict()
|
||||
retrospective_text = "\n".join(
|
||||
(item.get("summary") or item.get("summary_text") or "")
|
||||
for item in (retrospectives or [])
|
||||
if isinstance(item, dict)
|
||||
)
|
||||
|
||||
service = SkillService(self.db)
|
||||
for skill in await service.list_runtime_candidates(user_id, include_learned=include_learned):
|
||||
score, matched_terms = score_text_match(
|
||||
query_text,
|
||||
skill.name,
|
||||
skill.description,
|
||||
skill.instructions,
|
||||
retrospective_text,
|
||||
memory_context,
|
||||
)
|
||||
if score <= 0:
|
||||
continue
|
||||
entry = SkillShortlistEntry(
|
||||
skill_name=skill.name,
|
||||
source="database",
|
||||
source_id=skill.id,
|
||||
scope=[skill.agent_type, skill.visibility],
|
||||
status=skill.status,
|
||||
effectiveness=skill.effectiveness,
|
||||
score=score,
|
||||
matched_terms=matched_terms,
|
||||
rationale=(
|
||||
"Shadow skill matched current request; keep metadata-only injection."
|
||||
if skill.status == "shadow"
|
||||
else "Matched against DB skill metadata and instructions."
|
||||
),
|
||||
summary=skill.description or (skill.instructions[:160] if skill.instructions else None),
|
||||
injection_mode=(
|
||||
"metadata_only"
|
||||
if skill.status == "shadow"
|
||||
else choose_injection_mode(score, bool(skill.description or skill.instructions))
|
||||
),
|
||||
)
|
||||
self._upsert(deduped, entry)
|
||||
|
||||
registry = get_skill_registry()
|
||||
if not registry.list_all():
|
||||
try:
|
||||
registry.load_all()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for skill in registry.list_all():
|
||||
score, matched_terms = score_text_match(
|
||||
query_text,
|
||||
skill.name,
|
||||
skill.description,
|
||||
" ".join(skill.tags),
|
||||
" ".join(skill.triggers),
|
||||
skill.content[:400],
|
||||
retrospective_text,
|
||||
memory_context,
|
||||
)
|
||||
if score <= 0:
|
||||
continue
|
||||
entry = SkillShortlistEntry(
|
||||
skill_name=skill.name,
|
||||
source=skill.source,
|
||||
source_id=skill.source_id or skill.id,
|
||||
scope=skill.scope or list(skill.tags),
|
||||
status=skill.status,
|
||||
effectiveness=skill.effectiveness,
|
||||
score=score,
|
||||
matched_terms=matched_terms,
|
||||
rationale="Matched against local or external skill metadata.",
|
||||
summary=skill.description or skill.content[:160],
|
||||
injection_mode=choose_injection_mode(
|
||||
score,
|
||||
bool(skill.description or skill.content),
|
||||
),
|
||||
)
|
||||
self._upsert(deduped, entry)
|
||||
|
||||
return sorted(deduped.values(), key=lambda item: item.score, reverse=True)[:limit]
|
||||
|
||||
@staticmethod
|
||||
def _upsert(
|
||||
deduped: "OrderedDict[str, SkillShortlistEntry]",
|
||||
entry: SkillShortlistEntry,
|
||||
) -> None:
|
||||
existing = deduped.get(entry.skill_name)
|
||||
if existing is None or existing.score < entry.score:
|
||||
deduped[entry.skill_name] = entry
|
||||
|
||||
|
||||
def build_shortlisted_skill_context(
|
||||
shortlist: list[dict] | list[SkillShortlistEntry] | None,
|
||||
*,
|
||||
agent_type: str | None = None,
|
||||
) -> str:
|
||||
if not shortlist:
|
||||
return ""
|
||||
|
||||
entries: list[SkillShortlistEntry] = []
|
||||
for item in shortlist:
|
||||
entry = item if isinstance(item, SkillShortlistEntry) else SkillShortlistEntry.model_validate(item)
|
||||
if agent_type and entry.scope and agent_type not in entry.scope:
|
||||
continue
|
||||
entries.append(entry)
|
||||
|
||||
return render_skill_shortlist_context(entries)
|
||||
|
||||
|
||||
async def shortlist_skills_for_request(
|
||||
db,
|
||||
*,
|
||||
user_id: str,
|
||||
user_query: str,
|
||||
memory_context: str | None = None,
|
||||
retrospectives: list[dict] | None = None,
|
||||
include_learned: bool = True,
|
||||
limit: int = 3,
|
||||
) -> list[SkillShortlistEntry]:
|
||||
return await RuntimeSkillRetriever(db).shortlist(
|
||||
user_id=user_id,
|
||||
query_text=user_query,
|
||||
memory_context=memory_context,
|
||||
retrospectives=retrospectives,
|
||||
include_learned=include_learned,
|
||||
limit=limit,
|
||||
)
|
||||
Reference in New Issue
Block a user