HKUDS/DeepTutor · error · HubError

{self.name}: unrecognised skill detail shape

Error message

{self.name}: unrecognised skill detail shape

What it means

Raised by HubProvider.detail when the parsed JSON for /skills/{slug} is not a dict (e.g. a list or a string). The detail view needs an object with at least a nested 'skill' object or flat skill fields.

Source

Thrown at deeptutor/services/skill/hub.py:432

                    break
        if rows is None:
            raise HubError(f"{self.name}: unrecognised catalog response shape")
        out: list[dict[str, Any]] = []
        for row in rows:
            if isinstance(row, dict):
                listing = self._listing_from_row(row)
                if listing is not None:
                    out.append(listing)
        return out

    def detail(self, slug: str) -> dict[str, Any]:
        """Full metadata + SKILL.md body for one hub skill (browser detail view)."""
        try:
            payload = self._get(f"/skills/{slug}").json()
        except ValueError as exc:
            raise HubError(f"{self.name}: skill detail returned invalid JSON") from exc
        if not isinstance(payload, dict):
            raise HubError(f"{self.name}: unrecognised skill detail shape")
        skill = payload.get("skill") if isinstance(payload.get("skill"), dict) else payload
        listing = self._listing_from_row(skill) or {"slug": slug, "name": slug}
        # Owner sometimes lives at the envelope top level, not inside ``skill``.
        if not listing.get("owner") and isinstance(payload.get("owner"), dict):
            owner = payload["owner"]
            listing["owner"] = str(owner.get("displayName") or owner.get("handle") or "")
            listing["owner_url"] = str(owner.get("htmlUrl") or "")
        dist = payload.get("distTags") if isinstance(payload.get("distTags"), dict) else {}
        listing["version"] = str(dist.get("latest") or listing.get("version") or "")
        listing["content"] = str(skill.get("description") or "")
        # EduHub's ``tags`` field is a dist-tags map; the topical labels live in
        # ``keywords``. Fall back to a list-shaped ``tags`` for other hubs.
        keywords = skill.get("keywords")
        if not isinstance(keywords, list):
            keywords = skill.get("tags") if isinstance(skill.get("tags"), list) else []
        listing["tags"] = [str(t) for t in keywords if str(t).strip()]
        return listing

View on GitHub (pinned to 3e82f13042)

Solutions

  1. Confirm the endpoint returns an object: {"skill": {...}, "body": "...", ...}
  2. Search for the correct slug via hub.search() before calling detail
  3. Align hub API version with the client contract

Example fix

// before: [{"slug": "x", ...}]
// after:  {"skill": {"slug": "x", ...}, "body": "# ..."}
Defensive patterns

Strategy: type-guard

Validate before calling

p = httpx.get(f"{BASE}/skills/{slug}").json()
if not isinstance(p, dict): skip_detail(slug)

Type guard

def is_detail_dict(p: object) -> bool:
    return isinstance(p, dict)

Try / catch

try:
    hub.detail(slug)
except HubError as e:
    if "unrecognised skill detail shape" in str(e):
        return {"slug": slug, "name": slug}  # minimal fallback
    raise

Prevention

When it happens

Trigger: Calling detail(slug) when the hub returns a JSON array (e.g. a list of matches) or a scalar for a slug endpoint.

Common situations: Hub version that returns list-shaped search results for unknown slugs instead of an object; misrouted endpoint returning collection payloads.

Related errors


AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27). Data as JSON: /api/errors/bd1a6480f685c3d0. Report an issue: GitHub.