CoolFace
Apppublic

build-small-hackathon/hackathon-advisor

sourceHugging Facemitupdated 3mo agoView on Hugging Face
16likes
quest_cache.py273 linesDownload Raw Back to hackathon_advisor
1from __future__ import annotations2 3from collections.abc import Mapping, Sequence4from dataclasses import dataclass5from hashlib import sha2566import json7import os8from pathlib import Path9from typing import Any10from uuid import uuid411 12from hackathon_advisor.data import Project13from hackathon_advisor.quest_analysis import (14    METADATA_FIRST_QUEST_ANALYZER_SOURCE,15    MAX_QUEST_TOKENS,16    QuestAnalysisError,17    render_project_inference_prompt,18    resolve_quest_identity,19    validate_matches_by_project,20)21from hackathon_advisor.quest_taxonomy import (22    APP_PROMPT_CHAR_LIMIT,23    QUEST_PROFILES,24    QUEST_SYSTEM_PROMPT,25    README_PROMPT_CHAR_LIMIT,26)27from hackathon_advisor._text import utc_now28 29 30QUEST_CACHE_SCHEMA_VERSION = 131QUEST_CACHE_ROOT = Path("quest-cache") / "v1"32QUEST_PROMPT_VERSION = "quest-prompt-v2-metadata-first"33QUEST_ANALYZER_SOURCE = METADATA_FIRST_QUEST_ANALYZER_SOURCE34QUEST_GENERATION_CONFIG = {35    "enable_thinking": False,36    "temperature": 0.0,37    "do_sample": False,38    "max_new_tokens": MAX_QUEST_TOKENS,39}40 41 42@dataclass(frozen=True)43class QuestCacheIdentity:44    project_id: str45    prompt_hash: str46    taxonomy_hash: str47    analyzer_fingerprint: dict[str, Any]48    cache_key: str49 50    def to_dict(self) -> dict[str, Any]:51        return {52            "project_id": self.project_id,53            "prompt_hash": self.prompt_hash,54            "taxonomy_hash": self.taxonomy_hash,55            "analyzer_fingerprint": self.analyzer_fingerprint,56            "cache_key": self.cache_key,57        }58 59 60@dataclass(frozen=True)61class QuestCacheEntry:62    identity: QuestCacheIdentity63    matches: list[dict[str, Any]]64    source: str65    path: Path66    generated_at: str67 68 69@dataclass(frozen=True)70class QuestCacheLookup:71    identity: QuestCacheIdentity72    entry: QuestCacheEntry | None73    reason: str74 75 76def quest_analyzer_fingerprint_from_env(env: Mapping[str, str] | None = None) -> dict[str, Any]:77    model_id, adapter_id, adapter_revision = resolve_quest_identity(env)78    return {79        "source": QUEST_ANALYZER_SOURCE,80        "model_id": model_id,81        "adapter_id": adapter_id,82        "adapter_revision": adapter_revision,83        "adapter_digest": _local_artifact_digest(adapter_id),84        "prompt_version": QUEST_PROMPT_VERSION,85        "generation": dict(QUEST_GENERATION_CONFIG),86    }87 88 89def quest_taxonomy_hash() -> str:90    payload = {91        "system_prompt": QUEST_SYSTEM_PROMPT,92        "quest_profiles": list(QUEST_PROFILES),93        "readme_prompt_char_limit": README_PROMPT_CHAR_LIMIT,94        "app_prompt_char_limit": APP_PROMPT_CHAR_LIMIT,95        "prompt_version": QUEST_PROMPT_VERSION,96    }97    return sha256(_canonical_json(payload).encode("utf-8")).hexdigest()98 99 100def build_quest_cache_identity(101    project: Project,102    analyzer_fingerprint: Mapping[str, Any],103) -> QuestCacheIdentity:104    prompt_hash = sha256(render_project_inference_prompt(project).encode("utf-8")).hexdigest()105    taxonomy_hash = quest_taxonomy_hash()106    canonical_fingerprint = json.loads(_canonical_json(analyzer_fingerprint))107    key_payload = {108        "schema_version": QUEST_CACHE_SCHEMA_VERSION,109        "project_id": project.id,110        "prompt_hash": prompt_hash,111        "taxonomy_hash": taxonomy_hash,112        "analyzer_fingerprint": canonical_fingerprint,113    }114    cache_key = sha256(_canonical_json(key_payload).encode("utf-8")).hexdigest()115    return QuestCacheIdentity(116        project_id=project.id,117        prompt_hash=prompt_hash,118        taxonomy_hash=taxonomy_hash,119        analyzer_fingerprint=canonical_fingerprint,120        cache_key=cache_key,121    )122 123 124def quest_cache_path(cache_dir: Path, cache_key: str) -> Path:125    return cache_dir / QUEST_CACHE_ROOT / cache_key[:2] / f"{cache_key}.json"126 127 128def read_quest_cache_entry(129    cache_dir: Path,130    project: Project,131    analyzer_fingerprint: Mapping[str, Any],132) -> QuestCacheLookup:133    identity = build_quest_cache_identity(project, analyzer_fingerprint)134    path = quest_cache_path(cache_dir, identity.cache_key)135    if not path.is_file():136        return QuestCacheLookup(identity=identity, entry=None, reason="absent")137    try:138        payload = json.loads(path.read_text(encoding="utf-8"))139    except (OSError, json.JSONDecodeError) as error:140        return QuestCacheLookup(identity=identity, entry=None, reason=f"invalid_json:{error}")141    if not isinstance(payload, dict):142        return QuestCacheLookup(identity=identity, entry=None, reason="invalid_payload")143    try:144        entry = _validate_cache_payload(payload, project, identity, path)145    except QuestAnalysisError as error:146        return QuestCacheLookup(identity=identity, entry=None, reason=f"invalid_schema:{error}")147    return QuestCacheLookup(identity=identity, entry=entry, reason="hit")148 149 150def write_quest_cache_entry(151    cache_dir: Path,152    project: Project,153    analyzer_fingerprint: Mapping[str, Any],154    matches: Sequence[Mapping[str, Any]],155    *,156    source: str,157) -> QuestCacheEntry:158    identity = build_quest_cache_identity(project, analyzer_fingerprint)159    validated = validate_matches_by_project({project.id: list(matches)}, [project], source=source)160    generated_at = utc_now()161    payload = {162        "schema_version": QUEST_CACHE_SCHEMA_VERSION,163        "generated_at": generated_at,164        "source": validated.source,165        **identity.to_dict(),166        "matches": validated.matches_by_project[project.id],167    }168    path = quest_cache_path(cache_dir, identity.cache_key)169    path.parent.mkdir(parents=True, exist_ok=True)170    tmp_path = path.parent / f".{path.name}.{uuid4().hex}.tmp"171    tmp_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")172    os.replace(tmp_path, path)173    return QuestCacheEntry(174        identity=identity,175        matches=validated.matches_by_project[project.id],176        source=validated.source,177        path=path,178        generated_at=generated_at,179    )180 181 182def quest_cache_run_record(183    *,184    project: Project,185    identity: QuestCacheIdentity,186    matches: Sequence[Mapping[str, Any]],187    status: str,188    source: str,189    path: Path | None = None,190) -> dict[str, Any]:191    return {192        "project_id": project.id,193        "cache_key": identity.cache_key,194        "prompt_hash": identity.prompt_hash,195        "taxonomy_hash": identity.taxonomy_hash,196        "status": status,197        "source": source,198        "cache_path": path.as_posix() if path is not None else "",199        "matches": [dict(match) for match in matches],200    }201 202 203def build_quest_analysis_run_payload(204    *,205    run_id: str,206    analyzer_fingerprint: Mapping[str, Any],207    summary: Mapping[str, Any],208    project_records: Sequence[Mapping[str, Any]],209) -> dict[str, Any]:210    return {211        "schema_version": QUEST_CACHE_SCHEMA_VERSION,212        "run_id": run_id,213        "generated_at": utc_now(),214        "source": QUEST_ANALYZER_SOURCE,215        "analyzer_fingerprint": json.loads(_canonical_json(analyzer_fingerprint)),216        "taxonomy_hash": quest_taxonomy_hash(),217        "summary": dict(summary),218        "projects": [dict(record) for record in project_records],219    }220 221 222def _validate_cache_payload(223    payload: Mapping[str, Any],224    project: Project,225    identity: QuestCacheIdentity,226    path: Path,227) -> QuestCacheEntry:228    if payload.get("schema_version") != QUEST_CACHE_SCHEMA_VERSION:229        raise QuestAnalysisError("unsupported quest cache schema")230    for field, expected in identity.to_dict().items():231        if payload.get(field) != expected:232            raise QuestAnalysisError(f"cache {field} mismatch")233    source = str(payload.get("source") or QUEST_ANALYZER_SOURCE)234    validated = validate_matches_by_project({project.id: payload.get("matches") or []}, [project], source=source)235    generated_at = str(payload.get("generated_at") or "")236    return QuestCacheEntry(237        identity=identity,238        matches=validated.matches_by_project[project.id],239        source=validated.source,240        path=path,241        generated_at=generated_at,242    )243 244 245def _local_artifact_digest(raw_path: str) -> str:246    if not raw_path:247        return ""248    path = Path(raw_path).expanduser()249    if not path.is_absolute():250        path = (Path.cwd() / path).resolve()251    if not path.exists():252        return ""253    digest = sha256()254    if path.is_file():255        _hash_file_into(digest, path, path.name)256        return digest.hexdigest()257    for file_path in sorted(item for item in path.rglob("*") if item.is_file()):258        _hash_file_into(digest, file_path, file_path.relative_to(path).as_posix())259    return digest.hexdigest()260 261 262def _hash_file_into(digest: Any, file_path: Path, relative_name: str) -> None:263    digest.update(relative_name.encode("utf-8"))264    digest.update(b"\0")265    with file_path.open("rb") as handle:266        for chunk in iter(lambda: handle.read(1024 * 1024), b""):267            digest.update(chunk)268    digest.update(b"\0")269 270 271def _canonical_json(payload: Any) -> str:272    return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))273