usmanovrustam commited on
Commit
8d9da4b
·
1 Parent(s): 604b114

Pivot: Decoupled Qwen AI Intelligence Provider. Only Qwen model on HF.

Browse files
Files changed (4) hide show
  1. Dockerfile +7 -10
  2. qwen_api.py +170 -0
  3. requirements.qwen.txt +11 -0
  4. scraper.py +45 -0
Dockerfile CHANGED
@@ -25,19 +25,17 @@ RUN apt-get update && apt-get install -y \
25
  USER user
26
 
27
  # Install Python dependencies
28
- COPY --chown=user requirements.txt .
29
  RUN pip install --no-cache-dir --upgrade pip
30
 
31
  # CRITICAL: Use official pre-compiled CPU wheels for the Qwen engine to bypass OOM errors
32
  RUN pip install --no-cache-dir \
33
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \
34
- -r requirements.txt
35
 
36
- # Ensure huggingface_hub is installed for the auto-download feature
37
- RUN pip install --no-cache-dir huggingface_hub
38
-
39
- # Copy the application code
40
- COPY --chown=user . .
41
 
42
  # Create the models directory with correct permissions
43
  RUN mkdir -p ml_models && chmod 777 ml_models
@@ -45,6 +43,5 @@ RUN mkdir -p ml_models && chmod 777 ml_models
45
  # Expose the port HF Spaces expects
46
  EXPOSE 7860
47
 
48
- # Run the application
49
- # We use one worker because HF Free Tier (CPU) is best for single-threaded Qwen inference
50
- CMD ["gunicorn", "app:app", "-w", "1", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:7860", "--timeout", "600"]
 
25
  USER user
26
 
27
  # Install Python dependencies
28
+ COPY --chown=user requirements.qwen.txt .
29
  RUN pip install --no-cache-dir --upgrade pip
30
 
31
  # CRITICAL: Use official pre-compiled CPU wheels for the Qwen engine to bypass OOM errors
32
  RUN pip install --no-cache-dir \
33
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \
34
+ -r requirements.qwen.txt
35
 
36
+ # Copy only the essential AI application files
37
+ COPY --chown=user qwen_api.py .
38
+ COPY --chown=user scraper.py .
 
 
39
 
40
  # Create the models directory with correct permissions
41
  RUN mkdir -p ml_models && chmod 777 ml_models
 
43
  # Expose the port HF Spaces expects
44
  EXPOSE 7860
45
 
46
+ # Run the specialized Qwen API
47
+ CMD ["python", "qwen_api.py"]
 
qwen_api.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import threading
4
+ from fastapi import FastAPI, HTTPException, Request
5
+ from pydantic import BaseModel
6
+ from typing import List, Optional
7
+ from huggingface_hub import hf_hub_download
8
+ from llama_cpp import Llama
9
+ from datetime import datetime
10
+
11
+ # Configuration
12
+ MODEL_DIR = "ml_models"
13
+ MODEL_NAME = "qwen-3.5-4b.gguf"
14
+ QWEN_PATH = os.path.join(MODEL_DIR, MODEL_NAME)
15
+
16
+ app = FastAPI(title="Qwen Intelligence Provider", version="1.0.0")
17
+
18
+ # Global LLM instance
19
+ _llm = None
20
+ _lock = threading.Lock()
21
+
22
+ def get_qwen():
23
+ global _llm
24
+ with _lock:
25
+ if _llm is None:
26
+ if not os.path.exists(QWEN_PATH):
27
+ print("📥 Downloading Qwen 3.5 (4B) from Hugging Face...")
28
+ os.makedirs(MODEL_DIR, exist_ok=True)
29
+ hf_hub_download(
30
+ repo_id="bartowski/Qwen_Qwen3.5-4B-GGUF",
31
+ filename="Qwen3.5-4B-Instruct-Q4_K_M.gguf",
32
+ local_dir=MODEL_DIR,
33
+ local_dir_use_symlinks=False
34
+ )
35
+ downloaded = os.path.join(MODEL_DIR, "Qwen3.5-4B-Instruct-Q4_K_M.gguf")
36
+ if os.path.exists(downloaded):
37
+ os.rename(downloaded, QWEN_PATH)
38
+
39
+ print(f"🚀 Initializing Qwen engine at {QWEN_PATH}")
40
+ _llm = Llama(
41
+ model_path=QWEN_PATH,
42
+ n_ctx=2048,
43
+ n_threads=4, # Optimized for HF Space CPUs
44
+ verbose=False
45
+ )
46
+ return _llm
47
+
48
+ # --- Data Models ---
49
+
50
+ class KeywordRequest(BaseModel):
51
+ idea_text: str
52
+
53
+ class ValidationRequest(BaseModel):
54
+ title1: str
55
+ title2: str
56
+
57
+ class OverviewRequest(BaseModel):
58
+ neg_reviews: List[str]
59
+ pos_reviews: List[str]
60
+
61
+ class SynthesisRequest(BaseModel):
62
+ overview_text: str
63
+
64
+ class BattleRequest(BaseModel):
65
+ app_a_name: str
66
+ app_a_overview: str
67
+ app_b_name: str
68
+ app_b_overview: str
69
+
70
+ # --- Endpoints ---
71
+
72
+ @app.get("/")
73
+ async def root():
74
+ return {"status": "online", "model": "Qwen 3.5 4B", "service": "Intelligence Provider"}
75
+
76
+ @app.post("/keywords")
77
+ async def extract_keywords(req: KeywordRequest):
78
+ llm = get_qwen()
79
+ prompt = f"""<|start_header_id|>system<|end_header_id|>
80
+ You are a expert market research assistant.
81
+ Extract 4-5 high-quality, comma-separated keywords or app types that would help find the most relevant competitors for the provided app idea description.
82
+ Output ONLY the comma-separated keywords. No conversational filler.
83
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
84
+ App Idea: {req.idea_text}
85
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
86
+ res = llm(prompt, max_tokens=100, stop=["<|eot_id|>"], echo=False)
87
+ return {"keywords": res['choices'][0]['text'].strip()}
88
+
89
+ @app.post("/validate")
90
+ async def validate_match(req: ValidationRequest):
91
+ llm = get_qwen()
92
+ prompt = f"""<|start_header_id|>system<|end_header_id|>
93
+ You are an expert AI product assistant. Reply ONLY with "YES" if these two titles represent the same application, or "NO" if they are clearly different products.
94
+ Example: "WhatsApp" and "WhatsApp Messenger" is YES. "WhatsApp" and "Instagram" is NO.
95
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
96
+ App Name 1: {req.title1}
97
+ App Name 2: {req.title2}
98
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
99
+ res = llm(prompt, max_tokens=10, stop=["<|eot_id|>"], echo=False)
100
+ return {"match": res['choices'][0]['text'].strip().upper()}
101
+
102
+ @app.post("/overview")
103
+ async def generate_overview(req: OverviewRequest):
104
+ llm = get_qwen()
105
+
106
+ def get_mini_summary(items, sentiment):
107
+ if not items: return ""
108
+ # Keep only a sample to avoid context overflow in mini-summaries
109
+ sample = items[:15]
110
+ prompt = f"""<|start_header_id|>system<|end_header_id|>
111
+ You are a technical analyst. Summarize the provided {sentiment} reviews into a single dense bullet point of insights.
112
+ Do NOT include any filler.
113
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
114
+ Reviews: {chr(10).join(['- ' + r[:200] for r in sample])}
115
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
116
+ res = llm(prompt, max_tokens=150, stop=["<|eot_id|>"], echo=False)
117
+ return res['choices'][0]['text'].strip()
118
+
119
+ neg_mini = get_mini_summary(req.neg_reviews, "negative")
120
+ pos_mini = get_mini_summary(req.pos_reviews, "positive")
121
+
122
+ master_prompt = f"""<|start_header_id|>system<|end_header_id|>
123
+ You are a senior product manager. Based on the hierarchical insights provided, write a deep 3-paragraph professional analysis.
124
+ Structure:
125
+ - Paragraph 1: UX/UI and technical critical complaints.
126
+ - Paragraph 2: Core value propositions and high-impact features.
127
+ - Paragraph 3: Future roadmap and growth strategy.
128
+ CRITICAL: Start response IMMEDIATELY. No intro.
129
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
130
+ NEGATIVE THEMES: {neg_mini}
131
+ POSITIVE THEMES: {pos_mini}
132
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
133
+
134
+ res = llm(master_prompt, max_tokens=600, stop=["<|eot_id|>"], echo=False)
135
+ return {"overview": res['choices'][0]['text'].strip()}
136
+
137
+ @app.post("/synthesis")
138
+ async def generate_synthesis(req: SynthesisRequest):
139
+ llm = get_qwen()
140
+ prompt = f"""<|start_header_id|>system<|end_header_id|>
141
+ You are a elite strategic advisor. Condense the provided multi-paragraph analysis into a single, punchy, high-impact paragraph (max 40 words).
142
+ Focus on the "bottom line". Output ONLY the paragraph.
143
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
144
+ Full Analysis: {req.overview_text}
145
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
146
+ res = llm(prompt, max_tokens=150, stop=["<|eot_id|>"], echo=False)
147
+ return {"synthesis": res['choices'][0]['text'].strip()}
148
+
149
+ @app.post("/battle")
150
+ async def generate_battle(req: BattleRequest):
151
+ llm = get_qwen()
152
+ prompt = f"""<|start_header_id|>system<|end_header_id|>
153
+ You are an elite competitive intelligence officer. Perform a "Neural Clash" analysis between two competing applications.
154
+ Contrast their strengths, expose their fatal flaws, and declare a strategic winner.
155
+ Structure:
156
+ 1. WINNING EDGE: Who has the superior UX?
157
+ 2. VULNERABILITY GAP: Biggest retention killer?
158
+ 3. STRATEGIC VERDICT: Superior growth trajectory?
159
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
160
+ APP A ({req.app_a_name}): {req.app_a_overview}
161
+ APP B ({req.app_b_name}): {req.app_b_overview}
162
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
163
+ res = llm(prompt, max_tokens=600, stop=["<|eot_id|>"], echo=False)
164
+ return {"battle": res['choices'][0]['text'].strip()}
165
+
166
+ if __name__ == "__main__":
167
+ import uvicorn
168
+ # Pre-warm model cache
169
+ get_qwen()
170
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.qwen.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optimized AI-Only Requirements for Qwen Intelligence Provider
2
+ --prefer-binary
3
+ --only-binary llama-cpp-python
4
+
5
+ fastapi==0.128.8
6
+ uvicorn[standard]==0.33.0
7
+ llama_cpp_python==0.3.19
8
+ huggingface_hub==0.36.2
9
+ python-dotenv==1.0.0
10
+ pydantic
11
+ requests
scraper.py CHANGED
@@ -130,8 +130,22 @@ def search_app_store(query):
130
 
131
  # Neural Config
132
  QWEN_PATH = "ml_models/qwen-3.5-4b.gguf"
 
 
133
  _Qwen = None
134
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  def get_qwen():
136
  global _Qwen
137
  if _Qwen: return _Qwen
@@ -197,6 +211,11 @@ Reviews to analyze:
197
  pos_mini = get_mini_summary(pos_clusters, "positive")
198
 
199
  # Phase 2: Master Synthesis
 
 
 
 
 
200
  master_prompt = f"""<|start_header_id|>system<|end_header_id|>
201
  You are a senior product manager. Based on the hierarchical insights provided, write a deep 3-paragraph professional analysis.
202
  Structure:
@@ -235,6 +254,12 @@ Directly output the paragraph. No filler.
235
  Full Analysis:
236
  {overview_text}
237
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
 
 
 
 
 
 
238
  try:
239
  res = llm(prompt, max_tokens=150, stop=["<|eot_id|>"], echo=False)
240
  return res['choices'][0]['text'].strip(' "\n\r')
@@ -274,6 +299,14 @@ APPLICATION B: {name_b}
274
  INTEL ON B: {analysis_b}
275
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
276
 
 
 
 
 
 
 
 
 
277
  try:
278
  res = llm(prompt, max_tokens=600, stop=["<|eot_id|>"], echo=False)
279
  return res['choices'][0]['text'].strip(' "\n\r')
@@ -295,6 +328,12 @@ Output ONLY the comma-separated keywords. No conversational filler.
295
  <|eot_id|><|start_header_id|>user<|end_header_id|>
296
  App Idea: {idea_text}
297
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
 
 
 
 
 
 
298
  try:
299
  res = llm(prompt, max_tokens=100, stop=["<|eot_id|>"], echo=False)
300
  return res['choices'][0]['text'].strip()
@@ -312,6 +351,12 @@ Example: "WhatsApp" and "WhatsApp Messenger" is YES. "WhatsApp" and "Instagram"
312
  App Name 1: {title1}
313
  App Name 2: {title2}
314
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
 
 
 
 
 
 
315
  try:
316
  ans_res = llm(prompt, max_tokens=10, stop=["<|eot_id|>"], echo=False)
317
  ans = ans_res['choices'][0]['text'].strip().upper()
 
130
 
131
  # Neural Config
132
  QWEN_PATH = "ml_models/qwen-3.5-4b.gguf"
133
+ # Hugging Face Intelligence Provider URL (if decoupled)
134
+ QWEN_REMOTE_URL = os.getenv("QWEN_REMOTE_URL")
135
  _Qwen = None
136
 
137
+ def call_remote_qwen(endpoint: str, payload: dict):
138
+ """Internal helper to communicate with the decoupled Qwen Intelligence Provider."""
139
+ if not QWEN_REMOTE_URL: return None
140
+ try:
141
+ url = f"{QWEN_REMOTE_URL.rstrip('/')}/{endpoint.lstrip('/')}"
142
+ res = requests.post(url, json=payload, timeout=60)
143
+ if res.status_code == 200:
144
+ return res.json()
145
+ except Exception as e:
146
+ print(f"[Remote AI Error] Failed to reach {endpoint}: {e}")
147
+ return None
148
+
149
  def get_qwen():
150
  global _Qwen
151
  if _Qwen: return _Qwen
 
211
  pos_mini = get_mini_summary(pos_clusters, "positive")
212
 
213
  # Phase 2: Master Synthesis
214
+ if QWEN_REMOTE_URL:
215
+ remote_res = call_remote_qwen("overview", {"neg_reviews": neg_clusters, "pos_reviews": pos_clusters})
216
+ if remote_res and "overview" in remote_res:
217
+ return remote_res["overview"]
218
+
219
  master_prompt = f"""<|start_header_id|>system<|end_header_id|>
220
  You are a senior product manager. Based on the hierarchical insights provided, write a deep 3-paragraph professional analysis.
221
  Structure:
 
254
  Full Analysis:
255
  {overview_text}
256
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
257
+
258
+ if QWEN_REMOTE_URL:
259
+ remote_res = call_remote_qwen("synthesis", {"overview_text": overview_text})
260
+ if remote_res and "synthesis" in remote_res:
261
+ return remote_res["synthesis"]
262
+
263
  try:
264
  res = llm(prompt, max_tokens=150, stop=["<|eot_id|>"], echo=False)
265
  return res['choices'][0]['text'].strip(' "\n\r')
 
299
  INTEL ON B: {analysis_b}
300
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
301
 
302
+ if QWEN_REMOTE_URL:
303
+ remote_res = call_remote_qwen("battle", {
304
+ "app_a_name": name_a, "app_a_overview": analysis_a,
305
+ "app_b_name": name_b, "app_b_overview": analysis_b
306
+ })
307
+ if remote_res and "battle" in remote_res:
308
+ return remote_res["battle"]
309
+
310
  try:
311
  res = llm(prompt, max_tokens=600, stop=["<|eot_id|>"], echo=False)
312
  return res['choices'][0]['text'].strip(' "\n\r')
 
328
  <|eot_id|><|start_header_id|>user<|end_header_id|>
329
  App Idea: {idea_text}
330
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
331
+
332
+ if QWEN_REMOTE_URL:
333
+ remote_res = call_remote_qwen("keywords", {"idea_text": idea_text})
334
+ if remote_res and "keywords" in remote_res:
335
+ return remote_res["keywords"]
336
+
337
  try:
338
  res = llm(prompt, max_tokens=100, stop=["<|eot_id|>"], echo=False)
339
  return res['choices'][0]['text'].strip()
 
351
  App Name 1: {title1}
352
  App Name 2: {title2}
353
  <|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
354
+
355
+ if QWEN_REMOTE_URL:
356
+ remote_res = call_remote_qwen("validate", {"title1": title1, "title2": title2})
357
+ if remote_res and "match" in remote_res:
358
+ return remote_res["match"]
359
+
360
  try:
361
  ans_res = llm(prompt, max_tokens=10, stop=["<|eot_id|>"], echo=False)
362
  ans = ans_res['choices'][0]['text'].strip().upper()