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, )