Files
JARVIS/backend/app/agents/skills/retriever.py

154 lines
5.2 KiB
Python

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