File size: 6,742 Bytes
c274c89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c10516
 
c274c89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""
AURA Model Router — Domain-aware routing with fallback and cache.
CPU-safe for Hugging Face Spaces free tier.
"""

import os
import hashlib
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta

logger = logging.getLogger("AURA.Router")

MAX_HISTORY = 10

CODE_KEYWORDS = [
    "code", "python", "javascript", "typescript", "function", "class",
    "def ", "import ", "const ", "let ", "var ", "print(", "return",
    "implement", "algorithm", "bug", "debug", "error", "exception",
    "api", "endpoint", "route", "database", "sql", "query", "docker",
    "git", "commit", "push", "deploy", "test", "unit test",
]
MATH_KEYWORDS = [
    "math", "equation", "calculate", "solve", "derivative", "integral",
    "algebra", "geometry", "statistics", "probability", "matrix",
    "linear", "quadratic", "polynomial", "function f(", "summation",
]
CREATIVE_KEYWORDS = [
    "write", "story", "poem", "essay", "creative", "imagine",
    "describe", "narrate", "fiction", "fantasy", "character",
    "plot", "dialogue", "metaphor", "lyrics", "song",
]
ANALYSIS_KEYWORDS = [
    "analyze", "explain", "compare", "contrast", "summarize",
    "evaluate", "interpret", "why", "how does", "what is the",
    "difference between", "pros and cons", "advantages",
]


class ResponseCache:
    def __init__(self, max_size: int = 100, ttl_minutes: int = 30):
        self.max_size = max_size
        self.ttl = timedelta(minutes=ttl_minutes)
        self._cache: Dict[str, Dict[str, Any]] = {}

    def _make_key(self, prompt: str, model: str) -> str:
        raw = f"{prompt.strip().lower()}|{model}"
        return hashlib.md5(raw.encode()).hexdigest()

    def get(self, prompt: str, model: str) -> Optional[str]:
        key = self._make_key(prompt, model)
        entry = self._cache.get(key)
        if entry is None:
            return None
        if datetime.now() - entry["ts"] > self.ttl:
            del self._cache[key]
            return None
        return entry["response"]

    def set(self, prompt: str, model: str, response: str) -> None:
        key = self._make_key(prompt, model)
        if len(self._cache) >= self.max_size:
            oldest = min(self._cache.keys(), key=lambda k: self._cache[k]["ts"])
            del self._cache[oldest]
        self._cache[key] = {"response": response, "ts": datetime.now()}

    def clear(self) -> None:
        self._cache.clear()


class ModelRouter:
    def __init__(
        self,
        primary_model: str = "microsoft/Phi-3-mini-4k-instruct",
        fallback_model: str = "microsoft/Phi-3-mini-4k-instruct",
        hf_token: Optional[str] = None,
        use_cache: bool = True,
    ):
        self.primary_model = primary_model
        self.fallback_model = fallback_model
        self.hf_token = hf_token or os.getenv("HF_TOKEN", "")
        self.cache = ResponseCache() if use_cache else None
        self._client = None

    @property
    def client(self):
        if self._client is None:
            from huggingface_hub import InferenceClient
            self._client = InferenceClient(token=self.hf_token)
        return self._client

    def classify_domain(self, prompt: str) -> str:
        lower = prompt.lower()
        scores = {
            "code": sum(1 for kw in CODE_KEYWORDS if kw in lower),
            "math": sum(1 for kw in MATH_KEYWORDS if kw in lower),
            "creative": sum(1 for kw in CREATIVE_KEYWORDS if kw in lower),
            "analysis": sum(1 for kw in ANALYSIS_KEYWORDS if kw in lower),
        }
        if len(prompt.split()) < 3:
            return "conversation"
        best = max(scores, key=scores.get)
        return best if scores[best] > 0 else "conversation"

    def select_model(self, domain: str) -> str:
        return self.primary_model

    def query(
        self,
        prompt: str,
        system_prompt: str,
        max_tokens: int = 512,
        temperature: float = 0.7,
        history: Optional[List[Dict]] = None,
    ) -> Dict[str, Any]:
        domain = self.classify_domain(prompt)
        model = self.primary_model

        if self.cache:
            cached = self.cache.get(prompt, model)
            if cached:
                return {
                    "response": cached,
                    "model_used": model,
                    "domain": domain,
                    "cached": True,
                    "error": None,
                }

        messages = [{"role": "system", "content": system_prompt}]
        if history:
            for msg in history[-MAX_HISTORY:]:
                messages.append(msg)
        messages.append({"role": "user", "content": prompt})

        try:
            result = self._query_model(model, messages, max_tokens, temperature)
            if self.cache:
                self.cache.set(prompt, model, result)
            return {
                "response": result,
                "model_used": model,
                "domain": domain,
                "cached": False,
                "error": None,
            }
        except Exception as exc:
            logger.warning("Primary model failed: %s", exc)

        try:
            result = self._query_model(model, messages, max_tokens, temperature)
            return {
                "response": result,
                "model_used": model,
                "domain": domain,
                "cached": False,
                "error": None,
            }
        except Exception as exc:
            logger.error("All models failed: %s", exc)
            return {
                "response": None,
                "model_used": None,
                "domain": domain,
                "cached": False,
                "error": str(exc),
            }

    def _query_model(self, model_id: str, messages: List[Dict], max_tokens: int, temperature: float) -> str:
        result = self.client.chat_completion(
            model=model_id,
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
        )
        return result.choices[0].message.content.strip()

    def health_check(self) -> Dict[str, Any]:
        try:
            test = self.client.chat_completion(
                model=self.primary_model,
                messages=[{"role": "user", "content": "ping"}],
                max_tokens=5,
            )
            primary_ok = bool(test.choices)
        except Exception:
            primary_ok = False

        return {
            "status": "ok" if primary_ok else "degraded",
            "primary_model": self.primary_model,
            "primary_online": primary_ok,
            "fallback_model": self.fallback_model,
            "cache_size": len(self.cache._cache) if self.cache else 0,
        }