mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-09 16:32:21 +02:00
Squash Odysseus development history
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Semantic ranking for the native Odysseus skill registry.
|
||||
|
||||
This deliberately reuses an already-ready ToolIndex embedding lane. Skill search
|
||||
must never trigger a cold embedding-model load in the foreground request path;
|
||||
the existing lexical matcher remains the fallback until startup prewarm finishes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, List, Sequence
|
||||
|
||||
from src.embedding_lanes import LANE_FASTEMBED
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CACHE_LIMIT = 16
|
||||
_cache_lock = threading.Lock()
|
||||
_corpus_cache: "OrderedDict[str, List[List[float]]]" = OrderedDict()
|
||||
|
||||
|
||||
def _semantic_text(skill: Dict[str, Any]) -> str:
|
||||
parts = [
|
||||
f"Skill: {skill.get('name', '')}",
|
||||
str(skill.get("description") or skill.get("title") or ""),
|
||||
f"Use when: {skill.get('when_to_use') or skill.get('problem') or ''}",
|
||||
]
|
||||
tags = skill.get("tags") or []
|
||||
if tags:
|
||||
parts.append("Tags: " + ", ".join(str(tag) for tag in tags[:24]))
|
||||
procedure = skill.get("procedure") or skill.get("steps") or []
|
||||
if procedure:
|
||||
parts.append("Procedure: " + " ".join(str(step) for step in procedure[:8]))
|
||||
# Imported skills can be very large. Ranking needs intent, not full reference
|
||||
# material or bundled templates.
|
||||
return "\n".join(parts)[:6000]
|
||||
|
||||
|
||||
def _choose_lane(index: Any) -> Any:
|
||||
lanes = list(getattr(index, "embedding_lanes", ()) or ())
|
||||
return next((lane for lane in lanes if getattr(lane, "name", "") == LANE_FASTEMBED), None) or (
|
||||
lanes[0] if lanes else None
|
||||
)
|
||||
|
||||
|
||||
def _cache_key(lane: Any, documents: Sequence[str]) -> str:
|
||||
lane_id = str(getattr(lane, "fingerprint", "") or getattr(lane, "name", "unknown"))
|
||||
digest = hashlib.sha256()
|
||||
digest.update(lane_id.encode("utf-8"))
|
||||
for document in documents:
|
||||
digest.update(b"\0")
|
||||
digest.update(document.encode("utf-8", errors="replace"))
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _document_vectors(lane: Any, documents: Sequence[str]) -> List[List[float]]:
|
||||
key = _cache_key(lane, documents)
|
||||
with _cache_lock:
|
||||
cached = _corpus_cache.get(key)
|
||||
if cached is not None:
|
||||
_corpus_cache.move_to_end(key)
|
||||
return cached
|
||||
|
||||
vectors = [list(vector) for vector in lane.encode(documents)]
|
||||
with _cache_lock:
|
||||
_corpus_cache[key] = vectors
|
||||
_corpus_cache.move_to_end(key)
|
||||
while len(_corpus_cache) > _CACHE_LIMIT:
|
||||
_corpus_cache.popitem(last=False)
|
||||
return vectors
|
||||
|
||||
|
||||
def semantic_skill_scores(query: str, skills: Sequence[Dict[str, Any]]) -> Dict[int, float]:
|
||||
"""Return cosine-like scores keyed by input position, or ``{}`` on fallback."""
|
||||
if not query.strip() or not skills:
|
||||
return {}
|
||||
try:
|
||||
# This accessor has no initialization side effect. Startup owns model
|
||||
# loading; foreground skill matching remains fast and deterministic.
|
||||
from src.tool_index import get_ready_tool_index
|
||||
|
||||
index = get_ready_tool_index()
|
||||
if index is None:
|
||||
return {}
|
||||
lane = _choose_lane(index)
|
||||
if lane is None:
|
||||
return {}
|
||||
|
||||
documents = [_semantic_text(skill) for skill in skills]
|
||||
vectors = _document_vectors(lane, documents)
|
||||
query_vectors = lane.encode([query[:4000]])
|
||||
if not query_vectors:
|
||||
return {}
|
||||
query_vector = list(query_vectors[0])
|
||||
|
||||
scores: Dict[int, float] = {}
|
||||
for index_position, vector in enumerate(vectors):
|
||||
if len(vector) != len(query_vector) or not vector:
|
||||
continue
|
||||
# EmbeddingLane.encode requests normalized vectors, so their dot
|
||||
# product is cosine similarity without another numerical dependency.
|
||||
score = sum(float(left) * float(right) for left, right in zip(query_vector, vector))
|
||||
scores[index_position] = max(-1.0, min(1.0, score))
|
||||
return scores
|
||||
except Exception as exc:
|
||||
logger.debug("Semantic skill ranking unavailable; using lexical fallback: %s", exc)
|
||||
return {}
|
||||
|
||||
|
||||
def reset_skill_index_cache() -> None:
|
||||
"""Clear process-local vectors after embedding configuration changes/tests."""
|
||||
with _cache_lock:
|
||||
_corpus_cache.clear()
|
||||
Reference in New Issue
Block a user