Spaces:
Sleeping
Sleeping
| """ | |
| Bharat Tech Atlas β Chat Engine Core | |
| Implements lazy model loading, keyword fallbacks, web search, LLM generation, | |
| and safety checks (prompt injection + XSS sanitization). | |
| """ | |
| import logging | |
| from typing import Optional, List, Tuple | |
| from ..security import ( | |
| validate_chat_message, | |
| detect_prompt_injection, | |
| sanitize_response_text, | |
| escape_html, | |
| audit_log, | |
| ) | |
| from .config import ( | |
| MODEL_ID, | |
| MAX_NEW_TOKENS, | |
| TEMPERATURE, | |
| TOP_P, | |
| DEVICE_GPU, | |
| DEVICE_CPU, | |
| KEYWORD_RESPONSES, | |
| NEEDS_SEARCH_TRIGGERS, | |
| WEB_SEARCH_MAX_RESULTS, | |
| WEB_SEARCH_QUERY_PREFIX, | |
| SYSTEM_PROMPT, | |
| SYSTEM_PROMPT_WITH_WEB, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # βββ Lazy-loaded pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _chat_pipeline = None | |
| def _get_chat_pipeline(): | |
| """Lazy-load Qwen2.5-0.5B-Instruct. Returns None if transformers unavailable.""" | |
| global _chat_pipeline | |
| if _chat_pipeline is not None: | |
| return _chat_pipeline | |
| try: | |
| from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline | |
| import torch | |
| device = DEVICE_GPU if torch.cuda.is_available() else DEVICE_CPU | |
| dtype = torch.float16 if device == 0 else torch.float32 | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) | |
| model = AutoModelForCausalLM.from_pretrained( | |
| MODEL_ID, trust_remote_code=True, torch_dtype=dtype, | |
| device_map="auto" if device == 0 else None, | |
| ) | |
| _chat_pipeline = pipeline( | |
| "text-generation", model=model, tokenizer=tokenizer, | |
| device=device, do_sample=True, temperature=TEMPERATURE, | |
| top_p=TOP_P, max_new_tokens=MAX_NEW_TOKENS, | |
| ) | |
| logger.info("Chat model loaded: %s", MODEL_ID) | |
| return _chat_pipeline | |
| except Exception as e: | |
| logger.warning("Could not load chat model: %s", e) | |
| _chat_pipeline = False | |
| return None | |
| def keyword_response(user_text: str) -> Optional[str]: | |
| """Return a keyword-match answer without loading the LLM.""" | |
| lowered = user_text.lower() | |
| for kw, resp in KEYWORD_RESPONSES.items(): | |
| if kw in lowered: | |
| return resp | |
| return None | |
| def needs_web_search(text: str) -> bool: | |
| lowered = text.lower() | |
| return any(t in lowered for t in NEEDS_SEARCH_TRIGGERS) | |
| async def web_search(query: str, max_results: int = WEB_SEARCH_MAX_RESULTS) -> List[dict]: | |
| """Search DuckDuckGo for fresh news/articles.""" | |
| results = [] | |
| try: | |
| from duckduckgo_search import DDGS | |
| with DDGS() as ddgs: | |
| for r in ddgs.text(query, max_results=max_results): | |
| title = escape_html(r.get("title", ""))[:200] | |
| url = r.get("href", "") | |
| snippet = escape_html(r.get("body", ""))[:400] | |
| results.append({"title": title, "url": url, "snippet": snippet}) | |
| except Exception as e: | |
| logger.warning("Web search failed: %s", e) | |
| return results | |
| def generate_with_model( | |
| messages: List[dict], | |
| web_results: Optional[List[dict]] = None, | |
| req_id: str = "unknown", | |
| ) -> Tuple[str, dict]: | |
| """Generate a response via Qwen. Returns (text, safety_info).""" | |
| pipeline = _get_chat_pipeline() | |
| safety = { | |
| "model_used": False, | |
| "xss_detected": False, | |
| "injection_score": 0.0, | |
| } | |
| if not pipeline: | |
| if web_results: | |
| lines = ["Here are the latest search results:"] | |
| for r in web_results[:5]: | |
| lines.append(f"- {r['title']}: {r['snippet'][:200]}...") | |
| return "\n".join(lines), safety | |
| return ("I'm running in lightweight mode. Ask about unicorns, fintech, SaaS,", | |
| safety) | |
| if web_results: | |
| search_ctx = "\n\n".join([ | |
| f"[{i+1}] {r['title']}\n{r['snippet']}\nSource: {r['url']}" | |
| for i, r in enumerate(web_results[:6]) | |
| ]) | |
| chat = [ | |
| {"role": "system", "content": SYSTEM_PROMPT_WITH_WEB + f"\n\nSearch results:\n{search_ctx}\n"}, | |
| ] | |
| else: | |
| chat = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| for m in messages: | |
| chat.append({"role": m["role"], "content": m["content"]}) | |
| try: | |
| prompt = pipeline.tokenizer.apply_chat_template( | |
| chat, tokenize=False, add_generation_prompt=True | |
| ) | |
| outputs = pipeline(prompt, return_full_text=False, max_new_tokens=MAX_NEW_TOKENS) | |
| raw = outputs[0]["generated_text"].strip() | |
| safety["model_used"] = True | |
| safety["injection_score"] = detect_prompt_injection(raw) | |
| text = sanitize_response_text(raw) | |
| safety["xss_detected"] = text != raw | |
| return text, safety | |
| except Exception as e: | |
| logger.error("Chat generation failed: %s", e) | |
| if web_results: | |
| lines = ["I found these results but couldn't process them fully:"] | |
| for r in web_results[:5]: | |
| lines.append(f"- {r['title']}: {r['snippet'][:200]}...") | |
| return "\n".join(lines), safety | |
| return "I'm having trouble processing that. Try asking about Indian startups or sectors.", safety | |