llm-ready-data / app /services /semantic_router_service.py
light-infer-chat's picture
ok
52e2d2c
Raw
History Blame Contribute Delete
3.61 kB
from __future__ import annotations
import logging
from collections import defaultdict
from typing import Any, Optional
import numpy as np
from app.services.embeddings_service import EmbeddingService
_logger = logging.getLogger(__name__)
_DIMENSION = 384
class SemanticRouterService:
def __init__(self, embedding_service: EmbeddingService) -> None:
self._embedding_service = embedding_service
def route(
self,
query: str,
routes: list[dict[str, Any]],
threshold: float = 0.7,
) -> dict[str, Any]:
if not query or not query.strip():
return {"success": False, "name": None, "models": [], "error": "Query is empty."}
if not routes:
return {"success": False, "name": None, "models": [], "error": "No routes provided."}
if not self._embedding_service.is_loaded(_DIMENSION):
return {"success": False, "name": None, "models": [], "error": "Embedding model not loaded."}
all_utterances: list[str] = []
utterance_to_route: list[int] = []
for idx, route in enumerate(routes):
utterances = route.get("utterances", [])
if not utterances:
continue
all_utterances.extend(utterances)
utterance_to_route.extend([idx] * len(utterances))
if not all_utterances:
return {"success": False, "name": None, "models": [], "error": "No utterances found in any route."}
try:
all_texts = all_utterances + [query]
embeddings = self._embedding_service.generate_embedding(all_texts, _DIMENSION)
except Exception as exc:
_logger.error("Embedding failed: %s", exc)
return {"success": False, "name": None, "models": [], "error": str(exc)}
utterance_embs = np.array(embeddings[:-1], dtype=np.float32)
query_emb = np.array(embeddings[-1], dtype=np.float32).reshape(1, -1)
similarities = (utterance_embs @ query_emb.T).flatten()
best_idx = int(np.argmax(similarities))
best_score = float(similarities[best_idx])
# Route-level scoring: take the max score per route
route_scores: dict[int, list[float]] = defaultdict(list)
for i, score in enumerate(similarities):
route_scores[utterance_to_route[i]].append(float(score))
route_best_scores = {rid: max(scores) for rid, scores in route_scores.items()}
sorted_routes = sorted(route_best_scores.items(), key=lambda x: x[1], reverse=True)
best_route_score = sorted_routes[0][1]
second_best_route_score = sorted_routes[1][1] if len(sorted_routes) > 1 else 1.0
route_margin = best_route_score - second_best_route_score
if best_score < threshold or route_margin < 0.001:
return {
"success": True,
"name": None,
"models": [],
"error": None,
"confidence": best_score,
"margin": route_margin,
"threshold": threshold,
"matched_utterance": all_utterances[best_idx],
}
matched_route_idx = utterance_to_route[best_idx]
matched_route = routes[matched_route_idx]
return {
"success": True,
"name": matched_route.get("name"),
"models": matched_route.get("models", []),
"error": None,
"confidence": best_score,
"margin": route_margin,
"threshold": threshold,
"matched_utterance": all_utterances[best_idx],
}