Ram2005 commited on
Commit
0f23ed7
Β·
verified Β·
1 Parent(s): 563315f

Upload backend/chat/engine.py

Browse files
Files changed (1) hide show
  1. backend/chat/engine.py +151 -0
backend/chat/engine.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bharat Tech Atlas β€” Chat Engine Core
3
+ Implements lazy model loading, keyword fallbacks, web search, LLM generation,
4
+ and safety checks (prompt injection + XSS sanitization).
5
+ """
6
+ import logging
7
+ from typing import Optional, List, Tuple
8
+
9
+ from ..security import (
10
+ validate_chat_message,
11
+ detect_prompt_injection,
12
+ sanitize_response_text,
13
+ escape_html,
14
+ audit_log,
15
+ )
16
+ from .config import (
17
+ MODEL_ID,
18
+ MAX_NEW_TOKENS,
19
+ TEMPERATURE,
20
+ TOP_P,
21
+ DEVICE_GPU,
22
+ DEVICE_CPU,
23
+ KEYWORD_RESPONSES,
24
+ NEEDS_SEARCH_TRIGGERS,
25
+ WEB_SEARCH_MAX_RESULTS,
26
+ WEB_SEARCH_QUERY_PREFIX,
27
+ SYSTEM_PROMPT,
28
+ SYSTEM_PROMPT_WITH_WEB,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ─── Lazy-loaded pipeline ───────────────────────────────────────────────────────
34
+ _chat_pipeline = None
35
+
36
+
37
+ def _get_chat_pipeline():
38
+ """Lazy-load Qwen2.5-0.5B-Instruct. Returns None if transformers unavailable."""
39
+ global _chat_pipeline
40
+ if _chat_pipeline is not None:
41
+ return _chat_pipeline
42
+ try:
43
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
44
+ import torch
45
+
46
+ device = DEVICE_GPU if torch.cuda.is_available() else DEVICE_CPU
47
+ dtype = torch.float16 if device == 0 else torch.float32
48
+
49
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
50
+ model = AutoModelForCausalLM.from_pretrained(
51
+ MODEL_ID, trust_remote_code=True, torch_dtype=dtype,
52
+ device_map="auto" if device == 0 else None,
53
+ )
54
+ _chat_pipeline = pipeline(
55
+ "text-generation", model=model, tokenizer=tokenizer,
56
+ device=device, do_sample=True, temperature=TEMPERATURE,
57
+ top_p=TOP_P, max_new_tokens=MAX_NEW_TOKENS,
58
+ )
59
+ logger.info("Chat model loaded: %s", MODEL_ID)
60
+ return _chat_pipeline
61
+ except Exception as e:
62
+ logger.warning("Could not load chat model: %s", e)
63
+ _chat_pipeline = False
64
+ return None
65
+
66
+
67
+ def keyword_response(user_text: str) -> Optional[str]:
68
+ """Return a keyword-match answer without loading the LLM."""
69
+ lowered = user_text.lower()
70
+ for kw, resp in KEYWORD_RESPONSES.items():
71
+ if kw in lowered:
72
+ return resp
73
+ return None
74
+
75
+
76
+ def needs_web_search(text: str) -> bool:
77
+ lowered = text.lower()
78
+ return any(t in lowered for t in NEEDS_SEARCH_TRIGGERS)
79
+
80
+
81
+ async def web_search(query: str, max_results: int = WEB_SEARCH_MAX_RESULTS) -> List[dict]:
82
+ """Search DuckDuckGo for fresh news/articles."""
83
+ results = []
84
+ try:
85
+ from duckduckgo_search import DDGS
86
+ with DDGS() as ddgs:
87
+ for r in ddgs.text(query, max_results=max_results):
88
+ title = escape_html(r.get("title", ""))[:200]
89
+ url = r.get("href", "")
90
+ snippet = escape_html(r.get("body", ""))[:400]
91
+ results.append({"title": title, "url": url, "snippet": snippet})
92
+ except Exception as e:
93
+ logger.warning("Web search failed: %s", e)
94
+ return results
95
+
96
+
97
+ def generate_with_model(
98
+ messages: List[dict],
99
+ web_results: Optional[List[dict]] = None,
100
+ req_id: str = "unknown",
101
+ ) -> Tuple[str, dict]:
102
+ """Generate a response via Qwen. Returns (text, safety_info)."""
103
+ pipeline = _get_chat_pipeline()
104
+ safety = {
105
+ "model_used": False,
106
+ "xss_detected": False,
107
+ "injection_score": 0.0,
108
+ }
109
+
110
+ if not pipeline:
111
+ if web_results:
112
+ lines = ["Here are the latest search results:"]
113
+ for r in web_results[:5]:
114
+ lines.append(f"- {r['title']}: {r['snippet'][:200]}...")
115
+ return "\n".join(lines), safety
116
+ return ("I'm running in lightweight mode. Ask about unicorns, fintech, SaaS,",
117
+ safety)
118
+
119
+ if web_results:
120
+ search_ctx = "\n\n".join([
121
+ f"[{i+1}] {r['title']}\n{r['snippet']}\nSource: {r['url']}"
122
+ for i, r in enumerate(web_results[:6])
123
+ ])
124
+ chat = [
125
+ {"role": "system", "content": SYSTEM_PROMPT_WITH_WEB + f"\n\nSearch results:\n{search_ctx}\n"},
126
+ ]
127
+ else:
128
+ chat = [{"role": "system", "content": SYSTEM_PROMPT}]
129
+
130
+ for m in messages:
131
+ chat.append({"role": m["role"], "content": m["content"]})
132
+
133
+ try:
134
+ prompt = pipeline.tokenizer.apply_chat_template(
135
+ chat, tokenize=False, add_generation_prompt=True
136
+ )
137
+ outputs = pipeline(prompt, return_full_text=False, max_new_tokens=MAX_NEW_TOKENS)
138
+ raw = outputs[0]["generated_text"].strip()
139
+ safety["model_used"] = True
140
+ safety["injection_score"] = detect_prompt_injection(raw)
141
+ text = sanitize_response_text(raw)
142
+ safety["xss_detected"] = text != raw
143
+ return text, safety
144
+ except Exception as e:
145
+ logger.error("Chat generation failed: %s", e)
146
+ if web_results:
147
+ lines = ["I found these results but couldn't process them fully:"]
148
+ for r in web_results[:5]:
149
+ lines.append(f"- {r['title']}: {r['snippet'][:200]}...")
150
+ return "\n".join(lines), safety
151
+ return "I'm having trouble processing that. Try asking about Indian startups or sectors.", safety