siddhm11 commited on
Commit
46e5f2b
·
verified ·
1 Parent(s): b112b48

Upload 2 files

Browse files
Files changed (2) hide show
  1. main.py +377 -0
  2. requirements.txt +19 -0
main.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ from datetime import datetime
4
+ from typing import List, Optional
5
+
6
+ # Third-party libraries
7
+ from fastapi import FastAPI, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel
10
+ from qdrant_client import QdrantClient
11
+ from qdrant_client.models import PointStruct, Distance, VectorParams, Filter, FieldCondition, MatchValue# Lazy import: from sentence_transformers import SentenceTransformer
12
+ from groq import Groq
13
+ from pymongo import MongoClient
14
+ from dotenv import load_dotenv
15
+
16
+ # --- 1. CONFIGURATION & SECRETS ---
17
+ # Load environment variables from .env file
18
+ load_dotenv()
19
+
20
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
21
+ MONGO_URI = os.getenv("MONGO_URI")
22
+ QDRANT_URL = os.getenv("QDRANT_URL", ":memory:")
23
+ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
24
+
25
+ # Free embedding model: all-MiniLM-L6-v2 (384-dim, Apache 2.0). No API key required.
26
+ EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
27
+
28
+ # Basic check to ensure keys are present (only warn at startup; fail on /enhance if missing)
29
+ if not GROQ_API_KEY:
30
+ print("⚠️ GROQ_API_KEY is missing from .env — /enhance will fail until you add it.")
31
+
32
+ # --- 2. SETUP CLIENTS ---
33
+
34
+ # A. FastAPI App
35
+ app = FastAPI()
36
+
37
+ # B. CORS (Critical for Chrome Extension)
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"], # Allows all origins
41
+ allow_credentials=True,
42
+ allow_methods=["*"], # Allows all methods (POST, GET, etc.)
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ # C. MongoDB (User Profiles & Logs) — optional; use in-memory fallback if unavailable
47
+ users_col = None
48
+ prompts_col = None
49
+ _in_memory_users = {} # fallback when MongoDB is not running
50
+
51
+ try:
52
+ mongo_client = MongoClient(
53
+ MONGO_URI or "mongodb://localhost:27017",
54
+ serverSelectionTimeoutMS=3000,
55
+ )
56
+ mongo_client.admin.command("ping")
57
+ db = mongo_client["prompt_engine_db"]
58
+ users_col = db["users"]
59
+ prompts_col = db["prompt_logs"]
60
+ print("✅ MongoDB Connected")
61
+ except Exception as e:
62
+ print(f"⚠️ MongoDB not available ({e}) — using in-memory fallback for profiles/logs.")
63
+
64
+ # D. Qdrant (Vector Memory)
65
+ qdrant = None
66
+ COLLECTION_NAME = "prompt_memory"
67
+
68
+ def init_qdrant():
69
+ """Lazily initialize Qdrant connection."""
70
+ global qdrant
71
+ if qdrant is None:
72
+ try:
73
+ qdrant = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
74
+
75
+ # 1. Check if collection exists
76
+ try:
77
+ collection_exists = qdrant.collection_exists(COLLECTION_NAME)
78
+ except (AttributeError, Exception):
79
+ try:
80
+ qdrant.get_collection(COLLECTION_NAME)
81
+ collection_exists = True
82
+ except:
83
+ collection_exists = False
84
+
85
+ # 2. Create collection if it doesn't exist
86
+ if not collection_exists:
87
+ try:
88
+ qdrant.create_collection(
89
+ collection_name=COLLECTION_NAME,
90
+ vectors_config=VectorParams(size=384, distance=Distance.COSINE),
91
+ )
92
+ print(f"✅ Created new Qdrant collection: '{COLLECTION_NAME}'")
93
+ except Exception as e:
94
+ if "409" in str(e) or "already exists" in str(e):
95
+ pass
96
+ else:
97
+ raise
98
+
99
+ # --- THE FIX: CREATE PAYLOAD INDEX FOR USER_ID ---
100
+ # This tells Qdrant: "Please optimize searches for 'user_id'"
101
+ try:
102
+ qdrant.create_payload_index(
103
+ collection_name=COLLECTION_NAME,
104
+ field_name="user_id",
105
+ field_schema="keyword" # 'keyword' is best for exact string matches like IDs
106
+ )
107
+ print("✅ Payload index for 'user_id' ensured.")
108
+ except Exception as e:
109
+ # If index already exists, Qdrant might return an error or ignore it.
110
+ # We catch it just in case, but usually it's safe.
111
+ print(f"ℹ️ Note on Indexing: {e}")
112
+
113
+ print(f"✅ Qdrant Connected ({QDRANT_URL})")
114
+ except Exception as e:
115
+ print(f"❌ Qdrant Connection Failed: {e}")
116
+ return qdrant
117
+
118
+ # E. AI Models — free local embeddings (MiniLM via sentence-transformers)
119
+ print("Embedding: free model (MiniLM) will load on first use")
120
+ EMBEDDING_MODEL = None
121
+ _embedding_unavailable = False
122
+
123
+ # Lazy-load Groq client to avoid initialization errors
124
+ groq_client = None
125
+
126
+ def get_groq_client():
127
+ """Lazily initialize Groq client."""
128
+ global groq_client
129
+ if groq_client is None:
130
+ try:
131
+ groq_client = Groq(api_key=GROQ_API_KEY)
132
+ except Exception as e:
133
+ print(f"⚠️ Warning: Groq client initialization failed: {e}")
134
+ return groq_client
135
+
136
+ # --- 3. DATA MODELS (Pydantic) ---
137
+
138
+ class UserProfile(BaseModel):
139
+ user_id: str
140
+ tech_stack: List[str] # e.g., ["React", "Python", "AWS"]
141
+ preferences: str # e.g., "Clean code, no comments"
142
+
143
+ class PromptRequest(BaseModel):
144
+ user_id: str
145
+ prompt: str # Matches 'prompt' sent from your Extension
146
+ platform: Optional[str] = "unknown"
147
+
148
+ # --- 4. HELPER FUNCTIONS ---
149
+
150
+ def get_embedding(text: str):
151
+ """Converts text to 384-dim vector using free MiniLM model (sentence-transformers). Returns None if unavailable."""
152
+ global EMBEDDING_MODEL, _embedding_unavailable
153
+ if _embedding_unavailable:
154
+ return None
155
+ if EMBEDDING_MODEL is None:
156
+ try:
157
+ from sentence_transformers import SentenceTransformer
158
+ print("⏳ Loading free embedding model (all-MiniLM-L6-v2)...")
159
+ # Prefer ONNX backend (lighter, CPU-friendly); fallback to default
160
+ try:
161
+ EMBEDDING_MODEL = SentenceTransformer(EMBEDDING_MODEL_NAME, backend="onnx")
162
+ print("✅ Embedding model loaded (ONNX backend)")
163
+ except Exception:
164
+ EMBEDDING_MODEL = SentenceTransformer(EMBEDDING_MODEL_NAME)
165
+ print("✅ Embedding model loaded (default backend)")
166
+ except Exception as e:
167
+ _embedding_unavailable = True
168
+ print(f"⚠️ Embedding unavailable: {e} — install: pip install sentence-transformers (or sentence-transformers[onnx] for CPU)")
169
+ return None
170
+ return EMBEDDING_MODEL.encode(text, convert_to_numpy=True).tolist()
171
+
172
+ def retrieve_context(user_id: str, query_text: str, limit: int = 3):
173
+ """
174
+ Finds similar past prompts and returns both the text context AND the highest similarity score.
175
+ Returns: (context_str, max_score)
176
+ """
177
+ global qdrant
178
+ qdrant = init_qdrant()
179
+
180
+ # Default return values if DB is down or empty
181
+ if qdrant is None:
182
+ return "No relevant past context found.", 0.0
183
+
184
+ query_vector = get_embedding(query_text)
185
+ if query_vector is None:
186
+ return "No relevant past context found.", 0.0
187
+
188
+ # Search with User ID Filter
189
+ results = qdrant.search(
190
+ collection_name=COLLECTION_NAME,
191
+ query_vector=query_vector,
192
+ query_filter=Filter(
193
+ must=[
194
+ FieldCondition(
195
+ key="user_id",
196
+ match=MatchValue(value=user_id)
197
+ )
198
+ ]
199
+ ),
200
+ limit=limit
201
+ )
202
+
203
+ print(f"\n🔍 Searching Memory for User '{user_id}'...")
204
+
205
+ context_str = ""
206
+ max_score = 0.0 # Track the highest score found
207
+
208
+ for hit in results:
209
+ # Update max_score if this hit is higher
210
+ if hit.score > max_score:
211
+ max_score = hit.score
212
+
213
+ payload = hit.payload
214
+ print(f" Found candidate (Score: {hit.score:.4f}): {payload.get('original_prompt')}")
215
+
216
+ # Only add to string if it passes the "relevance" threshold (0.25)
217
+ if hit.score > 0.25:
218
+ context_str += f"- Past Prompt: \"{payload.get('original_prompt')}\"\n"
219
+ context_str += f"- Refined Version: \"{payload.get('refined_prompt')}\"\n\n"
220
+
221
+ final_context = context_str if context_str else "No relevant past context found."
222
+
223
+ return final_context, max_score
224
+
225
+ # --- 5. API ENDPOINTS ---
226
+
227
+ @app.get("/")
228
+ def health_check():
229
+ return {"status": "running", "service": "Context-Aware Prompt Engine"}
230
+
231
+ @app.post("/users/register")
232
+ def register_user(profile: UserProfile):
233
+ """Creates or updates a user profile."""
234
+ if users_col is not None:
235
+ users_col.update_one(
236
+ {"user_id": profile.user_id},
237
+ {"$set": profile.dict()},
238
+ upsert=True,
239
+ )
240
+ else:
241
+ _in_memory_users[profile.user_id] = profile.dict()
242
+ return {"message": f"User {profile.user_id} registered successfully."}
243
+
244
+
245
+
246
+ SOTA_SYSTEM_PROMPT = """
247
+ You are a Principal Prompt Architect. Your goal is not to "fix" the user's prompt, but to translate their raw intent into a "SOTA" executable specification for an LLM.
248
+
249
+ ### THE PHILOSOPHY (The 7 Rules)
250
+ 1. **Clarity**: Eliminate ambiguity.
251
+ 2. **Context**: Inject User Tech Stack [{tech_stack}] & Preferences [{preferences}].
252
+ 3. **Tasks**: Break complex goals into a step-by-step "Chain of Thought".
253
+ 4. **Format**: Explicitly define the output format (JSON, Markdown, etc.).
254
+ 5. **Examples**: Request few-shot examples if abstract.
255
+ 6. **Role**: Assign a HYPER-SPECIFIC persona (e.g., "Senior Geo-Spatial Data Engineer").
256
+ 7. **Constraints**: Define Negative Constraints (what NOT to do).
257
+
258
+ ### YOUR PROTOCOL
259
+ 1. **Analyze**: Identify the user's core intent.
260
+ 2. **Architect**: Construct a prompt using the **CO-STAR+** framework:
261
+ - [ROLE]: Act as {{Specific Expert Role}}...
262
+ - [CONTEXT]: User context is {tech_stack}...
263
+ - [TASK]: Your specific objective is...
264
+ - [STRATEGY]: Before writing code, outline your step-by-step reasoning...
265
+ - [CONSTRAINTS]: Do NOT use...
266
+ - [OUTPUT]: Provide the answer in {{Specific Format}}...
267
+
268
+ ### INSTRUCTIONS
269
+ - Return ONLY the final refined prompt.
270
+ - Do NOT provide explanations.
271
+ - If the prompt is a question TO YOU (like "what is this?"), answer it as a helper.
272
+ """
273
+
274
+
275
+ @app.post("/enhance")
276
+ def enhance_prompt(request: PromptRequest):
277
+ start_time = time.time()
278
+
279
+ # 1. GET USER CONTEXT (MongoDB Priority)
280
+ user_data = None
281
+ if users_col is not None:
282
+ user_data = users_col.find_one({"user_id": request.user_id})
283
+ if user_data is None:
284
+ user_data = _in_memory_users.get(request.user_id, {})
285
+
286
+ # Defaults
287
+ ts_raw = user_data.get("tech_stack", ["General Python", "Data Science"])
288
+ tech_stack = ", ".join(ts_raw) if isinstance(ts_raw, list) else str(ts_raw)
289
+ preferences = user_data.get("preferences", "Clean, modular code with docstrings.")
290
+
291
+ # 2. RETRIEVE MEMORY
292
+ past_context, max_similarity = retrieve_context(request.user_id, request.prompt)
293
+
294
+ # 3. CONSTRUCT SOTA PROMPT
295
+ formatted_system = SOTA_SYSTEM_PROMPT.format(
296
+ tech_stack=tech_stack,
297
+ preferences=preferences
298
+ )
299
+
300
+ user_message = f"""
301
+ ### 1. MEMORY & PAST STRATEGIES
302
+ {past_context}
303
+
304
+ ### 2. RAW USER INPUT
305
+ "{request.prompt}"
306
+
307
+ ### 3. TASK
308
+ Apply the 7 Rules. Transform the raw input into a SOTA prompt.
309
+ Ensure you define a specific EXPERT ROLE and Negative Constraints.
310
+ """
311
+
312
+ enhanced_prompt = request.prompt # Fallback
313
+ try:
314
+ client = get_groq_client()
315
+ chat_completion = client.chat.completions.create(
316
+ messages=[
317
+ {"role": "system", "content": formatted_system},
318
+ {"role": "user", "content": user_message}
319
+ ],
320
+ model="openai/gpt-oss-120b",
321
+ temperature=0.3, # Low temp for precision
322
+ )
323
+ enhanced_prompt = chat_completion.choices[0].message.content
324
+ except Exception as e:
325
+ print(f"❌ Groq API Error: {e}")
326
+
327
+ # 4. LOGGING (MongoDB)
328
+
329
+ process_time = round(time.time() - start_time, 2)
330
+ log_id = "memory-only"
331
+ if prompts_col is not None:
332
+ try:
333
+ log_entry = {
334
+ "user_id": request.user_id,
335
+ "timestamp": datetime.now(),
336
+ "original": request.prompt,
337
+ "enhanced": enhanced_prompt,
338
+ "score": max_similarity,
339
+ "latency": process_time
340
+ }
341
+ res = prompts_col.insert_one(log_entry)
342
+ log_id = str(res.inserted_id)
343
+ except: pass # <--- HANDLE ERRORS HERE
344
+
345
+ # 5. MEMORY STORAGE (Qdrant)
346
+ # Only save if unique (similarity < 0.90)
347
+ if max_similarity < 0.90:
348
+ try:
349
+ vec = get_embedding(request.prompt)
350
+ if vec:
351
+ q_client = init_qdrant()
352
+ if q_client:
353
+ q_client.upsert(
354
+ collection_name=COLLECTION_NAME,
355
+ points=[PointStruct(
356
+ id=int(time.time()),
357
+ vector=vec,
358
+ payload={"user_id": request.user_id, "original_prompt": request.prompt, "refined_prompt": enhanced_prompt}
359
+ )]
360
+ )
361
+ print("💾 New strategy memorized.")
362
+ except: pass
363
+ else:
364
+ print(f"♻️ Redundancy detected (Score {max_similarity:.2f}). Skipping save.")
365
+
366
+ return {
367
+ "original": request.prompt,
368
+ "enhanced": enhanced_prompt,
369
+ "log_id": log_id,
370
+ "latency": process_time
371
+ }
372
+
373
+
374
+ # Run with: uvicorn main:app --reload
375
+
376
+ ## change content.js as well
377
+
requirements.txt CHANGED
@@ -1,10 +1,29 @@
 
 
 
 
 
 
 
 
 
 
1
  fastapi==0.110.0
2
  uvicorn==0.27.1
3
  pydantic==2.6.1
4
  python-dotenv==1.0.1
 
 
5
  qdrant-client==1.7.3
 
 
6
  sentence-transformers[onnx]>=3.0.0
 
 
7
  groq>=0.4.0
8
  httpx==0.24.1
 
 
9
  pymongo==4.6.1
 
10
  numpy<2
 
1
+ # fastapi==0.104.1
2
+ # uvicorn==0.24.0
3
+ # pydantic==2.5.0
4
+ # python-dotenv==1.0.0
5
+ # qdrant-client==1.16.2
6
+ # sentence-transformers==2.7.0
7
+ # huggingface-hub==0.19.0
8
+ # groq==0.9.0
9
+ # pymongo==4.6.0
10
+
11
  fastapi==0.110.0
12
  uvicorn==0.27.1
13
  pydantic==2.6.1
14
  python-dotenv==1.0.1
15
+
16
+ # Vector DB
17
  qdrant-client==1.7.3
18
+
19
+ # Free embeddings (MiniLM) — use [onnx] for CPU-only / lighter install
20
  sentence-transformers[onnx]>=3.0.0
21
+
22
+ # APIs
23
  groq>=0.4.0
24
  httpx==0.24.1
25
+
26
+ # DB
27
  pymongo==4.6.1
28
+
29
  numpy<2