findEthics commited on
Commit
c4b1964
·
1 Parent(s): b4e0ec0

Add Query Classification

Browse files
Files changed (1) hide show
  1. app.py +137 -112
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  from fastapi import FastAPI, HTTPException
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pydantic import BaseModel
@@ -7,6 +6,7 @@ from transformers import pipeline
7
  from duckduckgo_search import DDGS
8
  from typing import Optional, List, Dict, Any
9
  import logging
 
10
 
11
  # Configure logging
12
  logging.basicConfig(level=logging.INFO)
@@ -14,12 +14,12 @@ logger = logging.getLogger(__name__)
14
 
15
  # Initialize FastAPI app
16
  app = FastAPI(
17
- title="Open Source Chat API",
18
- description="A fully open source alternative to HF API using local models",
19
- version="1.0.0"
20
  )
21
 
22
- # Configure CORS for Android app access
23
  app.add_middleware(
24
  CORSMiddleware,
25
  allow_origins=["*"],
@@ -36,175 +36,200 @@ class ChatRequest(BaseModel):
36
 
37
  class ChatResponse(BaseModel):
38
  response: str
39
- search_results: Optional[List[Dict[str, Any]]] = None # Contains web search results if use_search is enabled
 
40
 
41
  class SearchRequest(BaseModel):
42
  query: str
43
  max_results: int = 5
44
 
 
 
45
  qa_pipeline = None
 
46
  ner_pipeline = None
47
 
48
- def load_ner_model():
49
- """Load Named Entity Recognition model"""
50
- global ner_pipeline
51
  try:
52
- # Check if GPU is available
53
  device = "cuda" if torch.cuda.is_available() else "cpu"
54
- logger.info(f"Using device: {device}")
55
-
56
- ner_pipeline = pipeline(
57
- "ner",
58
- model="dbmdz/bert-large-cased-finetuned-conll03-english",
59
- device=0 if device == "cuda" else -1,
60
- grouped_entities=True
61
  )
62
- logger.info("NER model loaded successfully!")
63
  except Exception as e:
64
- logger.error(f"Error loading NER model: {e}")
65
 
66
- # Function to load the local language model
67
- def load_model():
68
- """Load the local language model"""
69
  global qa_pipeline
70
-
71
  try:
72
-
73
- # Check if GPU is available
74
  device = "cuda" if torch.cuda.is_available() else "cpu"
75
- logger.info(f"Using device: {device}")
76
-
77
  qa_pipeline = pipeline(
78
  "question-answering",
79
  model="distilbert-base-uncased-distilled-squad",
80
  device=0 if device == "cuda" else -1
81
  )
82
-
83
- logger.info("Model loaded successfully!")
84
-
85
  except Exception as e:
86
- logger.error(f"Error loading model: {e}")
87
 
88
- def search_web(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
89
- """Search the web using DuckDuckGo"""
 
90
  try:
91
- ddgs = DDGS()
92
- results = []
 
 
 
 
 
 
 
93
 
94
- for result in ddgs.text(query,safesearch='off',max_results=10):
95
- results.append({
96
- "title": result.get("title", ""),
97
- "body": result.get("body", ""),
98
- "href": result.get("href", "")
99
- })
 
 
 
 
 
 
 
 
100
 
101
- return results
 
 
 
 
 
 
 
 
 
 
 
 
102
 
 
 
 
 
 
 
 
 
 
103
  except Exception as e:
104
  logger.error(f"Search error: {e}")
105
  return []
106
 
107
- def generate_response(prompt: str,search_context: str) -> str:
108
- """Generate response using local model"""
109
- try:
110
- if qa_pipeline is None:
111
- return "Model not loaded properly. Please try again."
112
-
113
- # Validate that qa_pipeline is a question-answering pipeline
114
- if not hasattr(qa_pipeline, "task") or qa_pipeline.task != "question-answering":
115
- return "Invalid pipeline type. Expected a question-answering pipeline."
116
-
117
- result = qa_pipeline(question=prompt, context=search_context)['answer']
118
- return result
119
-
120
- except Exception as e:
121
- logger.error(f"Generation error: {e}")
122
- return f"Sorry, I encountered an error: {str(e)}"
123
 
124
  @app.on_event("startup")
125
  async def startup_event():
126
- """Load model on startup"""
127
- load_model()
128
  load_ner_model()
129
 
130
- @app.get("/")
131
- async def root():
132
- """Health check endpoint"""
133
- return {
134
- "message": "Open Source Chat API is running!",
135
- "model_loaded": qa_pipeline is not None,
136
- "endpoints": {
137
- "chat": "/chat",
138
- "search": "/search",
139
- "docs": "/docs"
140
- }
141
- }
142
-
143
  @app.post("/chat", response_model=ChatResponse)
144
- async def chat(request: ChatRequest):
145
- """Main chat endpoint"""
146
  try:
147
- search_results = None
148
- search_context = None
149
 
150
- # Perform web search if requested
151
  if request.use_search:
152
-
153
- # Extract entities for focused search
154
  entities = ner_pipeline(request.prompt)
155
- logger.info(f"Identified entities: {entities}")
156
-
157
- # Create search query from entities
158
  search_terms = [
159
  ent["word"] for ent in entities
160
  if ent["entity_group"] in ["PER", "ORG", "LOC", "MISC"]
161
  ]
162
- search_query = " ".join(search_terms) if search_terms else request.prompt
163
- logger.info(f"Search query: {search_query}")
164
-
165
  search_results = search_web(search_query)
166
-
167
- search_context = "\n".join([
168
- f"- {result['title']}: {result['body'][:200]}..."
169
- for result in search_results[:min(len(search_results), 5)]
170
- ])
171
-
 
 
 
 
 
 
 
 
 
172
  else:
173
- search_context = request.prompt
174
-
175
- logger.info(f"Search context: {search_context}")
176
- if search_context:
177
- # Generate response
178
- response = generate_response(
179
- request.prompt,
180
- search_context
181
  )
182
- else:
183
- response = "No context available to generate a response."
 
 
 
 
 
 
 
 
184
 
185
  return ChatResponse(
186
  response=response,
 
187
  search_results=search_results if request.use_search else None
188
  )
189
 
190
  except Exception as e:
191
- logger.error(f"Chat endpoint error: {e}")
192
  raise HTTPException(status_code=500, detail=str(e))
193
 
194
  @app.post("/search")
195
- async def search(request: SearchRequest):
196
- """Web search endpoint"""
197
  try:
198
- results = search_web(request.query, request.max_results)
199
- return {"results": results}
200
-
201
  except Exception as e:
202
  logger.error(f"Search endpoint error: {e}")
203
  raise HTTPException(status_code=500, detail=str(e))
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  if __name__ == "__main__":
206
  import uvicorn
207
- import os
208
- host = os.getenv("HOST", "0.0.0.0")
209
- port = int(os.getenv("PORT", 7860)) # Changed from 8000 to 7860
210
- uvicorn.run(app, host=host, port=port)
 
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
 
6
  from duckduckgo_search import DDGS
7
  from typing import Optional, List, Dict, Any
8
  import logging
9
+ import re
10
 
11
  # Configure logging
12
  logging.basicConfig(level=logging.INFO)
 
14
 
15
  # Initialize FastAPI app
16
  app = FastAPI(
17
+ title="Enhanced Chat API with Dynamic Model Selection",
18
+ description="API with query classification and dynamic model loading for QA/Summarization",
19
+ version="2.0.0"
20
  )
21
 
22
+ # Configure CORS
23
  app.add_middleware(
24
  CORSMiddleware,
25
  allow_origins=["*"],
 
36
 
37
  class ChatResponse(BaseModel):
38
  response: str
39
+ task_type: str
40
+ search_results: Optional[List[Dict[str, Any]]] = None
41
 
42
  class SearchRequest(BaseModel):
43
  query: str
44
  max_results: int = 5
45
 
46
+ # Global pipelines with lazy loading
47
+ classifier_pipeline = None
48
  qa_pipeline = None
49
+ summarization_pipeline = None
50
  ner_pipeline = None
51
 
52
+ def load_classifier():
53
+ """Load zero-shot classification model"""
54
+ global classifier_pipeline
55
  try:
 
56
  device = "cuda" if torch.cuda.is_available() else "cpu"
57
+ classifier_pipeline = pipeline(
58
+ "zero-shot-classification",
59
+ model="valhalla/distilbart-mnli-12-3",
60
+ device=0 if device == "cuda" else -1
 
 
 
61
  )
62
+ logger.info("Zero-shot classifier loaded")
63
  except Exception as e:
64
+ logger.error(f"Classifier load error: {e}")
65
 
66
+ def load_qa_model():
67
+ """Load question-answering model on demand"""
 
68
  global qa_pipeline
 
69
  try:
 
 
70
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
 
71
  qa_pipeline = pipeline(
72
  "question-answering",
73
  model="distilbert-base-uncased-distilled-squad",
74
  device=0 if device == "cuda" else -1
75
  )
76
+ logger.info("QA model loaded")
 
 
77
  except Exception as e:
78
+ logger.error(f"QA model load error: {e}")
79
 
80
+ def load_summarization_model():
81
+ """Load summarization model on demand"""
82
+ global summarization_pipeline
83
  try:
84
+ device = "cuda" if torch.cuda.is_available() else "cpu"
85
+ summarization_pipeline = pipeline(
86
+ "summarization",
87
+ model="sshleifer/distilbart-cnn-6-6",
88
+ device=0 if device == "cuda" else -1
89
+ )
90
+ logger.info("Summarization model loaded")
91
+ except Exception as e:
92
+ logger.error(f"Summarization model load error: {e}")
93
 
94
+ def load_ner_model():
95
+ """Load NER model"""
96
+ global ner_pipeline
97
+ try:
98
+ device = "cuda" if torch.cuda.is_available() else "cpu"
99
+ ner_pipeline = pipeline(
100
+ "ner",
101
+ model="dbmdz/bert-large-cased-finetuned-conll03-english",
102
+ device=0 if device == "cuda" else -1,
103
+ grouped_entities=True
104
+ )
105
+ logger.info("NER model loaded")
106
+ except Exception as e:
107
+ logger.error(f"NER model load error: {e}")
108
 
109
+ def classify_query(prompt: str) -> str:
110
+ """Classify query using zero-shot learning"""
111
+ candidate_labels = ['question answering', 'summarization']
112
+ try:
113
+ result = classifier_pipeline(
114
+ prompt,
115
+ candidate_labels,
116
+ multi_label=False
117
+ )
118
+ return result['labels'][0]
119
+ except Exception as e:
120
+ logger.error(f"Classification failed: {e}")
121
+ return 'question answering' # Fallback to QA
122
 
123
+ def search_web(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
124
+ """Enhanced web search with error handling"""
125
+ try:
126
+ with DDGS() as ddgs:
127
+ return [{
128
+ "title": r.get("title", ""),
129
+ "body": re.sub(r'\s+', ' ', r.get("body", "")).strip(),
130
+ "href": r.get("href", "")
131
+ } for r in ddgs.text(query, safesearch='off', max_results=max_results)]
132
  except Exception as e:
133
  logger.error(f"Search error: {e}")
134
  return []
135
 
136
+ def format_search_context(results: List[Dict[str, Any]]) -> str:
137
+ """Create condensed context from search results"""
138
+ return "\n".join(
139
+ f"{i+1}. {res['title']}: {res['body'][:200]}"
140
+ for i, res in enumerate(results[:5])
141
+ )
 
 
 
 
 
 
 
 
 
 
142
 
143
  @app.on_event("startup")
144
  async def startup_event():
145
+ """Initialize core models on startup"""
146
+ load_classifier()
147
  load_ner_model()
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  @app.post("/chat", response_model=ChatResponse)
150
+ async def chat_endpoint(request: ChatRequest):
151
+ """Enhanced chat endpoint with dynamic model selection"""
152
  try:
153
+ search_results = []
154
+ search_context = ""
155
 
156
+ # Web search processing
157
  if request.use_search:
 
 
158
  entities = ner_pipeline(request.prompt)
 
 
 
159
  search_terms = [
160
  ent["word"] for ent in entities
161
  if ent["entity_group"] in ["PER", "ORG", "LOC", "MISC"]
162
  ]
163
+ search_query = " ".join(search_terms) or request.prompt
 
 
164
  search_results = search_web(search_query)
165
+ search_context = format_search_context(search_results)
166
+
167
+ # Query classification
168
+ task_type = classify_query(request.prompt)
169
+ logger.info(f"Classified task: {task_type}")
170
+
171
+ # Dynamic model loading
172
+ if task_type == 'question answering':
173
+ if not qa_pipeline:
174
+ load_qa_model()
175
+ response = qa_pipeline(
176
+ question=request.prompt,
177
+ context=search_context or request.prompt,
178
+ max_answer_len=100
179
+ )['answer']
180
  else:
181
+ if not summarization_pipeline:
182
+ load_summarization_model()
183
+ # Handle long contexts safely
184
+ inputs = summarization_pipeline.tokenizer(
185
+ search_context or request.prompt,
186
+ truncation=True,
187
+ max_length=1024,
188
+ return_tensors="pt"
189
  )
190
+ processed_context = summarization_pipeline.tokenizer.decode(
191
+ inputs['input_ids'][0],
192
+ skip_special_tokens=True
193
+ )
194
+ response = summarization_pipeline(
195
+ processed_context,
196
+ max_length=150,
197
+ min_length=30,
198
+ do_sample=False
199
+ )[0]['summary_text']
200
 
201
  return ChatResponse(
202
  response=response,
203
+ task_type=task_type,
204
  search_results=search_results if request.use_search else None
205
  )
206
 
207
  except Exception as e:
208
+ logger.error(f"Chat error: {e}")
209
  raise HTTPException(status_code=500, detail=str(e))
210
 
211
  @app.post("/search")
212
+ async def search_endpoint(request: SearchRequest):
213
+ """Search endpoint with improved error handling"""
214
  try:
215
+ return {"results": search_web(request.query, request.max_results)}
 
 
216
  except Exception as e:
217
  logger.error(f"Search endpoint error: {e}")
218
  raise HTTPException(status_code=500, detail=str(e))
219
 
220
+ @app.get("/health")
221
+ async def health_check():
222
+ """Enhanced health check with model status"""
223
+ return {
224
+ "status": "OK",
225
+ "models": {
226
+ "classifier": bool(classifier_pipeline),
227
+ "qa": bool(qa_pipeline),
228
+ "summarization": bool(summarization_pipeline),
229
+ "ner": bool(ner_pipeline)
230
+ }
231
+ }
232
+
233
  if __name__ == "__main__":
234
  import uvicorn
235
+ uvicorn.run(app, host="0.0.0.0", port=7860)