light-infer-chat commited on
Commit
275f972
·
1 Parent(s): a6a4880
app/api/v1/router.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
 
3
  from fastapi import APIRouter
4
 
5
- from app.api.v1 import batch, chat, code_executor, convert, database, embeddings, reconcile, scraper, sql_validator, system, web_search
6
  from app.api.verify import router as verify_router
7
 
8
  api_v1_router = APIRouter()
@@ -17,4 +17,6 @@ api_v1_router.include_router(reconcile.router, tags=["Reconcile"])
17
  api_v1_router.include_router(scraper.router, tags=["Web Scraping"])
18
  api_v1_router.include_router(web_search.router, tags=["Web Search"])
19
  api_v1_router.include_router(sql_validator.router, tags=["SQL Validator"])
 
 
20
  api_v1_router.include_router(chat.router, tags=["Chat"])
 
2
 
3
  from fastapi import APIRouter
4
 
5
+ from app.api.v1 import batch, chat, code_executor, convert, database, embeddings, reconcile, scraper, semantic_router, sql_validator, system, token_counter, web_search
6
  from app.api.verify import router as verify_router
7
 
8
  api_v1_router = APIRouter()
 
17
  api_v1_router.include_router(scraper.router, tags=["Web Scraping"])
18
  api_v1_router.include_router(web_search.router, tags=["Web Search"])
19
  api_v1_router.include_router(sql_validator.router, tags=["SQL Validator"])
20
+ api_v1_router.include_router(semantic_router.router, tags=["Semantic Router"])
21
+ api_v1_router.include_router(token_counter.router, tags=["Token Counter"])
22
  api_v1_router.include_router(chat.router, tags=["Chat"])
app/api/v1/semantic_router.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from fastapi import APIRouter, Depends
6
+
7
+ from app.api.deps import get_embeddings_service, require_auth
8
+ from app.models.schemas import SemanticRouterRequest, SemanticRouterResponse
9
+ from app.services.embeddings_service import EmbeddingService
10
+ from app.services.semantic_router_service import SemanticRouterService
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.post(
16
+ "/semantic-router/route",
17
+ response_model=SemanticRouterResponse,
18
+ summary="Route a query to the best matching route using semantic similarity",
19
+ )
20
+ async def route_query(
21
+ body: SemanticRouterRequest,
22
+ token: str = Depends(require_auth),
23
+ embedding_service: EmbeddingService = Depends(get_embeddings_service),
24
+ ) -> SemanticRouterResponse:
25
+ start = time.perf_counter()
26
+ svc = SemanticRouterService(embedding_service)
27
+ routes_dict = [r.model_dump() for r in body.routes]
28
+ result = svc.route(body.query, routes_dict, body.threshold)
29
+ elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
30
+ return SemanticRouterResponse(
31
+ success=result["success"],
32
+ time_ms=elapsed_ms,
33
+ name=result.get("name"),
34
+ models=result.get("models", []),
35
+ error=result.get("error"),
36
+ confidence=result.get("confidence"),
37
+ threshold=result.get("threshold"),
38
+ matched_utterance=result.get("matched_utterance"),
39
+ )
app/api/v1/token_counter.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from fastapi import APIRouter, Depends
6
+
7
+ from app.api.deps import require_auth
8
+ from app.models.domain import count_tokens
9
+ from app.models.schemas import TokenCountRequest, TokenCountResponse
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.post(
15
+ "/token/count",
16
+ response_model=TokenCountResponse,
17
+ summary="Count tokens in text using tiktoken",
18
+ )
19
+ async def count_text_tokens(
20
+ body: TokenCountRequest,
21
+ token: str = Depends(require_auth),
22
+ ) -> TokenCountResponse:
23
+ start = time.perf_counter()
24
+ try:
25
+ token_count = count_tokens(body.text, body.encoding)
26
+ elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
27
+ return TokenCountResponse(
28
+ success=True,
29
+ time_ms=elapsed_ms,
30
+ token_count=token_count,
31
+ char_count=len(body.text),
32
+ encoding=body.encoding,
33
+ )
34
+ except Exception as exc:
35
+ elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
36
+ return TokenCountResponse(
37
+ success=False,
38
+ time_ms=elapsed_ms,
39
+ error=str(exc),
40
+ )
app/models/schemas.py CHANGED
@@ -6,6 +6,44 @@ from typing import Any, Dict, List, Literal, Optional
6
  from pydantic import BaseModel, Field, field_validator, model_validator
7
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  class ConversionMetadata(BaseModel):
11
  source: str
 
6
  from pydantic import BaseModel, Field, field_validator, model_validator
7
 
8
 
9
+ class RouteConfig(BaseModel):
10
+ name: str = Field(..., min_length=1, description="Route name")
11
+ utterances: List[str] = Field(..., min_length=1, description="Example phrases for this route")
12
+ models: List[str] = Field(default_factory=list, description="Model identifiers assigned to this route")
13
+
14
+
15
+ class SemanticRouterRequest(BaseModel):
16
+ query: str = Field(..., min_length=1, max_length=50000, description="User query to route")
17
+ routes: List[RouteConfig] = Field(..., min_length=1, description="Route definitions")
18
+ threshold: float = Field(default=0.3, ge=0.0, le=1.0, description="Minimum similarity score to match")
19
+
20
+
21
+ class SemanticRouterResponse(BaseModel):
22
+ success: bool
23
+ time_ms: float
24
+ name: Optional[str] = None
25
+ models: List[str] = []
26
+ error: Optional[str] = None
27
+ confidence: Optional[float] = None
28
+ margin: Optional[float] = None
29
+ threshold: Optional[float] = None
30
+ matched_utterance: Optional[str] = None
31
+
32
+
33
+ class TokenCountRequest(BaseModel):
34
+ text: str = Field(..., min_length=1, max_length=1000000, description="Text to count tokens for")
35
+ encoding: str = Field(default="o200k_base", description="TikToken encoding name")
36
+
37
+
38
+ class TokenCountResponse(BaseModel):
39
+ success: bool
40
+ time_ms: float
41
+ token_count: int = 0
42
+ char_count: int = 0
43
+ encoding: str = "o200k_base"
44
+ error: Optional[str] = None
45
+
46
+
47
 
48
  class ConversionMetadata(BaseModel):
49
  source: str
app/services/semantic_router_service.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ import numpy as np
7
+
8
+ from app.services.embeddings_service import EmbeddingService
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+ _DIMENSION = 384
13
+
14
+
15
+ class SemanticRouterService:
16
+ def __init__(self, embedding_service: EmbeddingService) -> None:
17
+ self._embedding_service = embedding_service
18
+
19
+ def route(
20
+ self,
21
+ query: str,
22
+ routes: list[dict[str, Any]],
23
+ threshold: float = 0.7,
24
+ ) -> dict[str, Any]:
25
+ if not query or not query.strip():
26
+ return {"success": False, "name": None, "models": [], "error": "Query is empty."}
27
+ if not routes:
28
+ return {"success": False, "name": None, "models": [], "error": "No routes provided."}
29
+
30
+ if not self._embedding_service.is_loaded(_DIMENSION):
31
+ return {"success": False, "name": None, "models": [], "error": "Embedding model not loaded."}
32
+
33
+ all_utterances: list[str] = []
34
+ utterance_to_route: list[int] = []
35
+
36
+ for idx, route in enumerate(routes):
37
+ utterances = route.get("utterances", [])
38
+ if not utterances:
39
+ continue
40
+ all_utterances.extend(utterances)
41
+ utterance_to_route.extend([idx] * len(utterances))
42
+
43
+ if not all_utterances:
44
+ return {"success": False, "name": None, "models": [], "error": "No utterances found in any route."}
45
+
46
+ try:
47
+ all_texts = all_utterances + [query]
48
+ embeddings = self._embedding_service.generate_embedding(all_texts, _DIMENSION)
49
+ except Exception as exc:
50
+ _logger.error("Embedding failed: %s", exc)
51
+ return {"success": False, "name": None, "models": [], "error": str(exc)}
52
+
53
+ utterance_embs = np.array(embeddings[:-1], dtype=np.float32)
54
+ query_emb = np.array(embeddings[-1], dtype=np.float32).reshape(1, -1)
55
+
56
+ similarities = (utterance_embs @ query_emb.T).flatten()
57
+ best_idx = int(np.argmax(similarities))
58
+ best_score = float(similarities[best_idx])
59
+
60
+ sorted_scores = sorted(similarities, reverse=True)
61
+ margin = (sorted_scores[0] - sorted_scores[1]) if len(sorted_scores) > 1 else 1.0
62
+
63
+ if best_score < threshold or margin < 0.01:
64
+ return {
65
+ "success": True,
66
+ "name": None,
67
+ "models": [],
68
+ "error": None,
69
+ "confidence": best_score,
70
+ "margin": margin,
71
+ "threshold": threshold,
72
+ "matched_utterance": all_utterances[best_idx],
73
+ }
74
+
75
+ matched_route_idx = utterance_to_route[best_idx]
76
+ matched_route = routes[matched_route_idx]
77
+
78
+ return {
79
+ "success": True,
80
+ "name": matched_route.get("name"),
81
+ "models": matched_route.get("models", []),
82
+ "error": None,
83
+ "confidence": best_score,
84
+ "margin": margin,
85
+ "threshold": threshold,
86
+ "matched_utterance": all_utterances[best_idx],
87
+ }