siddhm11 commited on
Commit
4d5cbc7
·
1 Parent(s): 87ed774

Update backend: mode-aware enhance, saved prompts CRUD, voice-enhance, feedback endpoint

Browse files
backend/core/config.py CHANGED
@@ -20,10 +20,9 @@ class Settings:
20
  GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
21
  JWT_SECRET = os.getenv("JWT_SECRET", "unsafedefaultsecret")
22
  ALGORITHM = "HS256"
23
- GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI", "http://localhost:8000/auth/google/callback")
24
 
25
  # Constants
26
  EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
27
- COLLECTION_NAME = "prompt-ex"
28
 
29
  settings = Settings()
 
20
  GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
21
  JWT_SECRET = os.getenv("JWT_SECRET", "unsafedefaultsecret")
22
  ALGORITHM = "HS256"
 
23
 
24
  # Constants
25
  EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
26
+ COLLECTION_NAME = "prompt_memory"
27
 
28
  settings = Settings()
backend/core/database.py CHANGED
@@ -4,12 +4,13 @@ from qdrant_client import QdrantClient
4
  from qdrant_client.models import VectorParams, Distance
5
  from .config import settings
6
 
7
- #MongoDB
8
  class MongoDB:
9
  client: MongoClient = None
10
  db = None
11
  users_col = None
12
  prompts_col = None
 
13
 
14
  @classmethod
15
  def connect(cls):
@@ -22,63 +23,74 @@ class MongoDB:
22
  cls.db = cls.client["prompt_engine_db"]
23
  cls.users_col = cls.db["users"]
24
  cls.prompts_col = cls.db["prompt_logs"]
 
25
 
26
- # 1. Index for Users: Ensures fast lookups and unique user_ids
27
  cls.users_col.create_index("user_id", unique=True)
28
-
29
- # 2. Index for Logs: Speed up finding a user's history sorted by time
30
- # This matches your query: .find({"user_id": ...}).sort("timestamp", -1)
31
  cls.prompts_col.create_index([("user_id", 1), ("timestamp", -1)])
 
32
 
33
  print("✅ MongoDB Indexes Verified")
34
-
35
  print("✅ MongoDB Connected")
36
  except Exception as e:
37
  print(f"⚠️ MongoDB not available ({e}) — using in-memory fallback.")
38
  cls.users_col = None
39
  cls.prompts_col = None
 
40
 
41
  # Qdrant
42
  class QdrantDB:
43
  client: QdrantClient = None
 
 
 
44
 
45
  @classmethod
46
  def get_client(cls):
47
  if cls.client is None:
48
  try:
49
  cls.client = QdrantClient(url=settings.QDRANT_URL, api_key=settings.QDRANT_API_KEY)
50
-
51
- # Check/Create Collection
52
- try:
53
- if not cls.client.collection_exists(settings.COLLECTION_NAME):
54
- cls.client.create_collection(
55
- collection_name=settings.COLLECTION_NAME,
56
- vectors_config=VectorParams(size=384, distance=Distance.COSINE),
57
- )
58
- print(f"✅ Created new Qdrant collection: '{settings.COLLECTION_NAME}'")
59
- except Exception:
60
- # Fallback check
61
- try:
62
- cls.client.get_collection(settings.COLLECTION_NAME)
63
- except:
64
- pass # Creation might have failed or raced
65
-
66
- # Create Payload Index
67
- try:
68
- cls.client.create_payload_index(
69
- collection_name=settings.COLLECTION_NAME,
70
- field_name="user_id",
71
- field_schema="keyword"
72
- )
73
- except Exception:
74
- pass
75
-
76
  print(f"✅ Qdrant Connected ({settings.QDRANT_URL})")
77
  except Exception as e:
78
  print(f"❌ Qdrant Connection Failed: {e}")
79
  return None
 
 
 
 
 
 
 
80
  return cls.client
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  # In-Memory Fallbacks
83
  in_memory_users = {}
84
  in_memory_prompt_logs = []
 
 
4
  from qdrant_client.models import VectorParams, Distance
5
  from .config import settings
6
 
7
+ # MongoDB
8
  class MongoDB:
9
  client: MongoClient = None
10
  db = None
11
  users_col = None
12
  prompts_col = None
13
+ saved_prompts_col = None
14
 
15
  @classmethod
16
  def connect(cls):
 
23
  cls.db = cls.client["prompt_engine_db"]
24
  cls.users_col = cls.db["users"]
25
  cls.prompts_col = cls.db["prompt_logs"]
26
+ cls.saved_prompts_col = cls.db["saved_prompts"]
27
 
28
+ # Indexes
29
  cls.users_col.create_index("user_id", unique=True)
 
 
 
30
  cls.prompts_col.create_index([("user_id", 1), ("timestamp", -1)])
31
+ cls.saved_prompts_col.create_index("user_id")
32
 
33
  print("✅ MongoDB Indexes Verified")
 
34
  print("✅ MongoDB Connected")
35
  except Exception as e:
36
  print(f"⚠️ MongoDB not available ({e}) — using in-memory fallback.")
37
  cls.users_col = None
38
  cls.prompts_col = None
39
+ cls.saved_prompts_col = None
40
 
41
  # Qdrant
42
  class QdrantDB:
43
  client: QdrantClient = None
44
+ _collections_ready = False
45
+
46
+ SAVED_COLLECTION = "saved_prompt_vectors"
47
 
48
  @classmethod
49
  def get_client(cls):
50
  if cls.client is None:
51
  try:
52
  cls.client = QdrantClient(url=settings.QDRANT_URL, api_key=settings.QDRANT_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  print(f"✅ Qdrant Connected ({settings.QDRANT_URL})")
54
  except Exception as e:
55
  print(f"❌ Qdrant Connection Failed: {e}")
56
  return None
57
+
58
+ # Ensure collections exist (runs once per process)
59
+ if not cls._collections_ready and cls.client is not None:
60
+ cls._ensure_collection(settings.COLLECTION_NAME)
61
+ cls._ensure_collection(cls.SAVED_COLLECTION)
62
+ cls._collections_ready = True
63
+
64
  return cls.client
65
 
66
+ @classmethod
67
+ def _ensure_collection(cls, name: str):
68
+ """Create a 384-dim cosine collection if it doesn't exist, with user_id index."""
69
+ try:
70
+ cls.client.get_collection(name)
71
+ print(f"✔ Qdrant collection '{name}' ready")
72
+ except Exception:
73
+ # Collection doesn't exist — create it
74
+ try:
75
+ cls.client.create_collection(
76
+ collection_name=name,
77
+ vectors_config=VectorParams(size=384, distance=Distance.COSINE),
78
+ )
79
+ print(f"✅ Created Qdrant collection: '{name}'")
80
+ except Exception as e:
81
+ print(f"⚠️ Could not create collection '{name}': {e}")
82
+ return
83
+
84
+ try:
85
+ cls.client.create_payload_index(
86
+ collection_name=name,
87
+ field_name="user_id",
88
+ field_schema="keyword"
89
+ )
90
+ except Exception:
91
+ pass
92
+
93
  # In-Memory Fallbacks
94
  in_memory_users = {}
95
  in_memory_prompt_logs = []
96
+ in_memory_saved_prompts = {} # {prompt_id: {doc}}
backend/main.py CHANGED
@@ -2,7 +2,7 @@
2
  from fastapi import FastAPI
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from .core.database import MongoDB
5
- from .routers import auth, users, prompts
6
 
7
  app = FastAPI(title="Context-Aware Prompt Engine")
8
 
@@ -28,6 +28,7 @@ def health_check():
28
  app.include_router(auth.router)
29
  app.include_router(users.router)
30
  app.include_router(prompts.router)
 
31
 
32
  if __name__ == "__main__":
33
  import uvicorn
 
2
  from fastapi import FastAPI
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from .core.database import MongoDB
5
+ from .routers import auth, users, prompts, saved_prompts
6
 
7
  app = FastAPI(title="Context-Aware Prompt Engine")
8
 
 
28
  app.include_router(auth.router)
29
  app.include_router(users.router)
30
  app.include_router(prompts.router)
31
+ app.include_router(saved_prompts.router)
32
 
33
  if __name__ == "__main__":
34
  import uvicorn
backend/models/schemas.py CHANGED
@@ -24,3 +24,41 @@ class OTPRequest(BaseModel):
24
  class OTPVerify(BaseModel):
25
  email: str
26
  code: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  class OTPVerify(BaseModel):
25
  email: str
26
  code: str
27
+
28
+ # --- Saved Prompts ---
29
+
30
+ class SavedPromptCreate(BaseModel):
31
+ """Create a saved prompt. Only content is required."""
32
+ content: str
33
+ title: Optional[str] = None
34
+ tags: Optional[List[str]] = None
35
+ platform: Optional[str] = None
36
+
37
+ class SavedPromptUpdate(BaseModel):
38
+ """Update a saved prompt. All fields optional."""
39
+ content: Optional[str] = None
40
+ title: Optional[str] = None
41
+ tags: Optional[List[str]] = None
42
+
43
+ # --- Enhanced Enhance Request ---
44
+
45
+ class EnhanceRequest(BaseModel):
46
+ """
47
+ The main enhance endpoint payload.
48
+ - conversation_context: recent messages from the visible chat (scraped from DOM)
49
+ - mode: 'quick' | 'deep' | 'creative' — controls enhancement intensity
50
+ - selected_prompt_ids: IDs of saved prompts the user explicitly ticked
51
+ """
52
+ prompt: str
53
+ platform: Optional[str] = "unknown"
54
+ mode: Optional[str] = "deep" # quick | deep | creative
55
+ conversation_context: Optional[List[str]] = None
56
+ selected_prompt_ids: Optional[List[str]] = None
57
+
58
+ class FeedbackRequest(BaseModel):
59
+ """Thumbs up/down on an enhanced prompt."""
60
+ log_id: str
61
+ rating: str # "up" | "down"
62
+ original: Optional[str] = None
63
+ enhanced: Optional[str] = None
64
+
backend/requirements.txt CHANGED
@@ -31,4 +31,7 @@ numpy<2
31
 
32
  # Auth & Utilities
33
  pyjwt==2.8.0
34
- requests==2.31.0
 
 
 
 
31
 
32
  # Auth & Utilities
33
  pyjwt==2.8.0
34
+ requests==2.31.0
35
+
36
+ # File Upload (voice-to-prompt)
37
+ python-multipart
backend/routers/auth.py CHANGED
@@ -17,6 +17,12 @@ _otp_store = {}
17
  def request_otp(request: OTPRequest):
18
  email = request.email.strip().lower()
19
 
 
 
 
 
 
 
20
  # Generate 6-digit code
21
  import random
22
  code = f"{random.randint(100000, 999999)}"
@@ -81,7 +87,7 @@ def google_login():
81
  if not settings.GOOGLE_CLIENT_ID:
82
  raise HTTPException(status_code=500, detail="Server missing Google Client ID")
83
 
84
- redirect_uri = settings.GOOGLE_REDIRECT_URI
85
  scope = "openid email profile"
86
  auth_url = (
87
  f"https://accounts.google.com/o/oauth2/v2/auth?"
@@ -102,7 +108,7 @@ async def google_callback(code: str):
102
  "client_secret": settings.GOOGLE_CLIENT_SECRET,
103
  "code": code,
104
  "grant_type": "authorization_code",
105
- "redirect_uri": settings.GOOGLE_REDIRECT_URI
106
  }
107
 
108
  async with httpx.AsyncClient() as client:
 
17
  def request_otp(request: OTPRequest):
18
  email = request.email.strip().lower()
19
 
20
+ # ── DEMO BYPASS: ok@gmail.com gets instant login ──
21
+ if email == "ok@gmail.com":
22
+ _otp_store[email] = {"code": "000000", "expires": time.time() + 9999}
23
+ print(f"\n🔓 [DEMO] Bypass login for {email} — code: 000000\n")
24
+ return {"message": "OTP sent."}
25
+
26
  # Generate 6-digit code
27
  import random
28
  code = f"{random.randint(100000, 999999)}"
 
87
  if not settings.GOOGLE_CLIENT_ID:
88
  raise HTTPException(status_code=500, detail="Server missing Google Client ID")
89
 
90
+ redirect_uri = "http://localhost:8000/auth/google/callback"
91
  scope = "openid email profile"
92
  auth_url = (
93
  f"https://accounts.google.com/o/oauth2/v2/auth?"
 
108
  "client_secret": settings.GOOGLE_CLIENT_SECRET,
109
  "code": code,
110
  "grant_type": "authorization_code",
111
+ "redirect_uri": "http://localhost:8000/auth/google/callback"
112
  }
113
 
114
  async with httpx.AsyncClient() as client:
backend/routers/prompts.py CHANGED
@@ -1,117 +1,217 @@
1
 
 
2
  import time
3
- from fastapi import APIRouter, Depends
4
- from ..models.schemas import PromptRequest, TrackRequest
 
 
5
  from ..core.security import verify_jwt
6
- from ..core.database import MongoDB, in_memory_users
7
  from ..services.memory_service import MemoryService
8
  from ..services.llm_service import get_groq_client
9
 
10
  router = APIRouter()
11
 
12
- SOTA_SYSTEM_PROMPT = """
13
- 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.
14
-
15
- ### THE PHILOSOPHY (The 7 Rules)
16
- 1. **Clarity**: Eliminate ambiguity.
17
- 2. **Context**: Inject User Tech Stack [{tech_stack}] & Preferences [{preferences}].
18
- 3. **Tasks**: Break complex goals into a step-by-step "Chain of Thought".
19
- 4. **Format**: Explicitly define the output format (JSON, Markdown, etc.).
20
- 5. **Examples**: Request few-shot examples if abstract.
21
- 6. **Role**: Assign a HYPER-SPECIFIC persona (e.g., "Senior Geo-Spatial Data Engineer").
22
- 7. **Constraints**: Define Negative Constraints (what NOT to do).
23
-
24
- ### YOUR PROTOCOL
25
- 1. **Analyze**: Identify the user's core intent.
26
- 2. **Architect**: Construct a prompt using the **CO-STAR+** framework:
27
- - [ROLE]: Act as {{Specific Expert Role}}...
28
- - [CONTEXT]: User context is {tech_stack}...
29
- - [TASK]: Your specific objective is...
30
- - [STRATEGY]: Before writing code, outline your step-by-step reasoning...
31
- - [CONSTRAINTS]: Do NOT use...
32
- - [OUTPUT]: Provide the answer in {{Specific Format}}...
33
-
34
- ### INSTRUCTIONS
35
- - Return ONLY the final refined prompt.
36
- - Do NOT provide explanations.
37
- - If the prompt is a question TO YOU (like "what is this?"), answer it as a helper.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  """
39
 
 
40
  @router.post("/track")
41
  def track_prompt(request: TrackRequest, user_id: str = Depends(verify_jwt)):
42
  """Silently learns from user prompts."""
43
  request.user_id = user_id
44
 
45
- # 0. Log to Short-Term
46
  MemoryService.log_prompt(
47
  user_id=request.user_id,
48
  original=request.prompt,
49
  source="passive_tracker"
50
  )
51
 
52
- # 1. Redundancy Check
53
  _, max_similarity = MemoryService.retrieve_context(request.user_id, request.prompt)
54
 
55
- if max_similarity > 0.90:
56
  return {"status": "skipped", "reason": "redundant"}
57
 
58
- # 2. Vectorize
59
  MemoryService.memorize_strategy(request.user_id, request.prompt, request.prompt)
60
  return {"status": "memorized"}
61
 
 
62
  @router.post("/enhance")
63
- def enhance_prompt(request: PromptRequest, user_id: str = Depends(verify_jwt)):
64
- request.user_id = user_id
 
 
 
 
 
 
 
 
65
  start_time = time.time()
 
 
 
 
66
 
67
- # 1. GET USER CONTEXT
68
  user_data = None
69
  if MongoDB.users_col is not None:
70
- user_data = MongoDB.users_col.find_one({"user_id": request.user_id})
71
  if user_data is None:
72
- user_data = in_memory_users.get(request.user_id, {})
73
 
74
- ts_raw = user_data.get("tech_stack", ["General Python", "Data Science"])
75
  tech_stack = ", ".join(ts_raw) if isinstance(ts_raw, list) else str(ts_raw)
76
- preferences = user_data.get("preferences", "Clean, modular code with docstrings.")
77
 
78
- # 2. RETRIEVE MEMORY
79
- past_context, max_similarity = MemoryService.retrieve_context(request.user_id, request.prompt)
 
 
 
80
 
81
- # 3. RECENT HISTORY
82
- recent_prompts = MemoryService.get_recent_prompts(request.user_id)
83
- recent_history_str = "\n".join([f"- {p}" for p in recent_prompts]) if recent_prompts else "No recent history."
 
 
 
 
 
 
84
 
85
- # 4. CONSTRUCT PROMPT
86
- formatted_system = SOTA_SYSTEM_PROMPT.format(
87
- tech_stack=tech_stack,
88
- preferences=preferences
 
 
89
  )
 
 
 
 
 
 
90
 
91
- user_message = f"""
92
- ### 1. RECENT ACTIVITY (Immediate Context)
93
- {recent_history_str}
94
-
95
- ### 2. LONG-TERM MEMORY & PAST STRATEGIES
96
- {past_context}
97
-
98
- ### 3. RAW USER INPUT
99
- "{request.prompt}"
 
 
 
 
 
 
100
 
101
- ### 4. TASK
102
- Apply the 7 Rules. Transform the raw input into a SOTA prompt.
103
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
 
105
  enhanced_prompt = request.prompt
106
  try:
107
  client = get_groq_client()
108
  chat_completion = client.chat.completions.create(
109
  messages=[
110
- {"role": "system", "content": formatted_system},
111
  {"role": "user", "content": user_message}
112
  ],
113
- model="openai/gpt-oss-120b",
114
- temperature=0.3,
115
  )
116
  enhanced_prompt = chat_completion.choices[0].message.content
117
  except Exception as e:
@@ -119,24 +219,148 @@ def enhance_prompt(request: PromptRequest, user_id: str = Depends(verify_jwt)):
119
 
120
  process_time = round(time.time() - start_time, 2)
121
 
122
- # 5. LOG
 
123
  log_id = MemoryService.log_prompt(
124
- user_id=request.user_id,
125
  original=request.prompt,
126
  enhanced=enhanced_prompt,
127
  score=max_similarity,
128
  latency=process_time,
129
  )
130
 
131
- # 6. MEMORIZE (if unique)
132
  if max_similarity < 0.90:
133
- MemoryService.memorize_strategy(request.user_id, request.prompt, enhanced_prompt)
134
- else:
135
- print(f"♻️ Redundancy detected (Score {max_similarity:.2f}). Skipping save.")
136
 
137
  return {
138
  "original": request.prompt,
139
  "enhanced": enhanced_prompt,
140
  "log_id": log_id,
141
- "latency": process_time
 
 
 
 
 
 
142
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ import io
3
  import time
4
+ import json
5
+ from bson import ObjectId
6
+ from fastapi import APIRouter, Depends, UploadFile, File, Form
7
+ from ..models.schemas import TrackRequest, EnhanceRequest, FeedbackRequest
8
  from ..core.security import verify_jwt
9
+ from ..core.database import MongoDB, in_memory_users, in_memory_saved_prompts
10
  from ..services.memory_service import MemoryService
11
  from ..services.llm_service import get_groq_client
12
 
13
  router = APIRouter()
14
 
15
+ # ══════════════════════════════════════════════════════════════
16
+ # SYSTEM PROMPTS Mode-Aware, Platform-Aware, Intent-Aware
17
+ # ══════════════════════════════════════════════════════════════
18
+
19
+ SYSTEM_PROMPT_BASE = """You are a Prompt Refinement Specialist. You take raw, incomplete human thoughts and transform them into the clearest, most effective prompt possible for an LLM.
20
+
21
+ ### ABSOLUTE RULE
22
+ Understand the user's TRUE INTENT first. Read the prompt literally.
23
+ - If it's about emotions refine as an emotional/psychology question.
24
+ - If it's about code refine as a technical spec.
25
+ - If it's creative refine as a creative brief.
26
+ - NEVER inject technical context into non-technical prompts.
27
+
28
+ ### CONVERSATION AWARENESS
29
+ You may receive the user's recent conversation history. This is CRITICAL context.
30
+ - "now add error handling" only makes sense if you know they were discussing React hooks.
31
+ - Use conversation history to resolve ambiguity, pronouns ("it", "this", "that"), and implicit references.
32
+ - Weave conversation context naturally — don't dump it, integrate it.
33
+
34
+ ### USING SAVED PROMPT CONTEXT
35
+ You may receive saved prompts (user-selected or auto-matched). Use them ONLY if topically relevant.
36
+ If a saved coding prompt appears but the user is asking about relationships — ignore it completely.
37
+
38
+ ### USER PROFILE (use ONLY for technical prompts)
39
+ - Tech stack: [{tech_stack}]
40
+ - Preferences: [{preferences}]
41
+ """
42
+
43
+ MODE_INSTRUCTIONS = {
44
+ "quick": """
45
+ ### MODE: QUICK
46
+ Keep it short and sharp. Minimal enhancement.
47
+ - Fix ambiguity and add just enough specificity
48
+ - Do NOT add frameworks, roles, or structures
49
+ - Output should be 1-3 sentences max
50
+ - Think: "What's the clearest way to ask this?"
51
+ """,
52
+ "deep": """
53
+ ### MODE: DEEP
54
+ Full structured enhancement. This is the power mode.
55
+ - For technical prompts: apply CO-STAR (Role, Context, Task, Strategy, Constraints, Output format)
56
+ - For non-technical: add depth, specificity, expert perspective, and structure
57
+ - Break complex asks into numbered parts
58
+ - Add useful constraints (what to do AND what not to do)
59
+ - The output should be comprehensive but natural — not a template
60
+ """,
61
+ "creative": """
62
+ ### MODE: CREATIVE
63
+ Loosen constraints. Encourage exploration and originality.
64
+ - Invite the LLM to think divergently
65
+ - Suggest multiple angles or perspectives
66
+ - Use open-ended framing ("explore", "what if", "imagine")
67
+ - Don't over-constrain — leave room for surprise
68
+ - Keep the tone warm and curious
69
+ """
70
+ }
71
+
72
+ PLATFORM_HINTS = {
73
+ "claude.ai": "The target LLM is Claude. Claude responds well to clear, direct instructions. Use natural prose rather than heavy formatting.",
74
+ "chatgpt.com": "The target LLM is ChatGPT. ChatGPT responds well to markdown structure — use headers, bullet points, and clear formatting.",
75
+ "gemini.google.com": "The target LLM is Gemini. Gemini prefers concise, focused questions with clear intent. Avoid excessive structure.",
76
+ "www.perplexity.ai": "The target LLM is Perplexity (search-focused). Frame prompts as clear research questions with specific information needs.",
77
+ "grok.com": "The target LLM is Grok. Grok appreciates direct, witty, and concise prompts. Keep instructions clear and don't over-formalize.",
78
+ "x.com": "The target LLM is Grok (via X). Grok appreciates direct, witty, and concise prompts. Keep instructions clear and don't over-formalize.",
79
+ }
80
+
81
+ OUTPUT_INSTRUCTION = """
82
+ ### OUTPUT
83
+ - Return ONLY the refined prompt. No explanations, no commentary, no labels.
84
+ - The refined prompt should feel like a natural, well-crafted message — not a rigid template.
85
+ - Match the user's language (English, Hindi, etc.).
86
  """
87
 
88
+
89
  @router.post("/track")
90
  def track_prompt(request: TrackRequest, user_id: str = Depends(verify_jwt)):
91
  """Silently learns from user prompts."""
92
  request.user_id = user_id
93
 
 
94
  MemoryService.log_prompt(
95
  user_id=request.user_id,
96
  original=request.prompt,
97
  source="passive_tracker"
98
  )
99
 
 
100
  _, max_similarity = MemoryService.retrieve_context(request.user_id, request.prompt)
101
 
102
+ if max_similarity > 0.95:
103
  return {"status": "skipped", "reason": "redundant"}
104
 
 
105
  MemoryService.memorize_strategy(request.user_id, request.prompt, request.prompt)
106
  return {"status": "memorized"}
107
 
108
+
109
  @router.post("/enhance")
110
+ def enhance_prompt(request: EnhanceRequest, user_id: str = Depends(verify_jwt)):
111
+ """
112
+ The core prompt engineering endpoint — intent-aware, mode-aware, platform-aware.
113
+
114
+ Context Priority:
115
+ 1. Conversation history (what's been discussed on the page)
116
+ 2. User-selected saved prompts
117
+ 3. Similarity-matched saved prompts
118
+ 4. User profile (only if technical)
119
+ """
120
  start_time = time.time()
121
+ mode = (request.mode or "deep").lower()
122
+ if mode not in MODE_INSTRUCTIONS:
123
+ mode = "deep"
124
+ platform = request.platform or "unknown"
125
 
126
+ # ── 1. USER PROFILE ──
127
  user_data = None
128
  if MongoDB.users_col is not None:
129
+ user_data = MongoDB.users_col.find_one({"user_id": user_id})
130
  if user_data is None:
131
+ user_data = in_memory_users.get(user_id, {})
132
 
133
+ ts_raw = user_data.get("tech_stack", [])
134
  tech_stack = ", ".join(ts_raw) if isinstance(ts_raw, list) else str(ts_raw)
135
+ preferences = user_data.get("preferences", "")
136
 
137
+ # ── 2. CONVERSATION CONTEXT ──
138
+ conversation_ctx = ""
139
+ if request.conversation_context and len(request.conversation_context) > 0:
140
+ msgs = request.conversation_context[-6:] # last 6 messages max
141
+ conversation_ctx = "\n".join([f"- {m}" for m in msgs])
142
 
143
+ # ── 3. USER-SELECTED SAVED PROMPTS ──
144
+ selected_context_parts = []
145
+ selected_ids = request.selected_prompt_ids or []
146
+
147
+ for pid in selected_ids:
148
+ doc = _fetch_saved_prompt(pid, user_id)
149
+ if doc:
150
+ label = doc.get("title") or "Saved Prompt"
151
+ selected_context_parts.append(f"[Selected by user] {label}: \"{doc['content']}\"")
152
 
153
+ # ── 4. SIMILARITY SEARCH ON SAVED PROMPTS ──
154
+ similar_saved = MemoryService.search_saved_prompts(
155
+ user_id=user_id,
156
+ query_text=request.prompt,
157
+ limit=3,
158
+ exclude_ids=selected_ids,
159
  )
160
+ similarity_context_parts = []
161
+ for item in similar_saved:
162
+ label = item.get("title") or "Saved Prompt"
163
+ similarity_context_parts.append(
164
+ f"[Auto-matched] {label}: \"{item['content']}\""
165
+ )
166
 
167
+ # ── 5. BUILD SYSTEM PROMPT ──
168
+ system_parts = [
169
+ SYSTEM_PROMPT_BASE.format(
170
+ tech_stack=tech_stack or "Not specified",
171
+ preferences=preferences or "Not specified"
172
+ ),
173
+ MODE_INSTRUCTIONS[mode],
174
+ ]
175
+
176
+ # Platform hint
177
+ if platform in PLATFORM_HINTS:
178
+ system_parts.append(f"### PLATFORM\n{PLATFORM_HINTS[platform]}")
179
+
180
+ system_parts.append(OUTPUT_INSTRUCTION)
181
+ system_prompt = "\n".join(system_parts)
182
 
183
+ # ── 6. BUILD USER MESSAGE ──
184
+ user_parts = []
185
+
186
+ # Conversation context (highest priority — it's the live thread)
187
+ if conversation_ctx:
188
+ user_parts.append(f"### RECENT CONVERSATION (what the user has been discussing)\n{conversation_ctx}")
189
+
190
+ # Saved prompt context
191
+ if selected_context_parts:
192
+ user_parts.append("### USER-SELECTED CONTEXT\n" + "\n".join(selected_context_parts))
193
+
194
+ if similarity_context_parts:
195
+ user_parts.append("### RELATED SAVED PROMPTS (use only if relevant)\n" + "\n".join(similarity_context_parts))
196
+
197
+ # The actual prompt
198
+ user_parts.append(f"### USER'S PROMPT\n\"{request.prompt}\"")
199
+
200
+ user_parts.append("### TASK\nRefine the user's prompt. Stay true to their intent. Use conversation context to resolve any ambiguity. If saved context is relevant, weave it in. If not, ignore it.")
201
+
202
+ user_message = "\n\n".join(user_parts)
203
 
204
+ # ── 7. CALL LLM ──
205
  enhanced_prompt = request.prompt
206
  try:
207
  client = get_groq_client()
208
  chat_completion = client.chat.completions.create(
209
  messages=[
210
+ {"role": "system", "content": system_prompt},
211
  {"role": "user", "content": user_message}
212
  ],
213
+ model="llama-3.3-70b-versatile",
214
+ temperature=0.2 if mode == "quick" else 0.4 if mode == "creative" else 0.3,
215
  )
216
  enhanced_prompt = chat_completion.choices[0].message.content
217
  except Exception as e:
 
219
 
220
  process_time = round(time.time() - start_time, 2)
221
 
222
+ # ── 8. LOG ──
223
+ max_similarity = similar_saved[0]["score"] if similar_saved else 0.0
224
  log_id = MemoryService.log_prompt(
225
+ user_id=user_id,
226
  original=request.prompt,
227
  enhanced=enhanced_prompt,
228
  score=max_similarity,
229
  latency=process_time,
230
  )
231
 
232
+ # ── 9. MEMORIZE (if unique) ──
233
  if max_similarity < 0.90:
234
+ MemoryService.memorize_strategy(user_id, request.prompt, enhanced_prompt)
 
 
235
 
236
  return {
237
  "original": request.prompt,
238
  "enhanced": enhanced_prompt,
239
  "log_id": log_id,
240
+ "latency": process_time,
241
+ "mode": mode,
242
+ "context_used": {
243
+ "selected": len(selected_context_parts),
244
+ "auto_matched": len(similarity_context_parts),
245
+ "conversation_messages": len(request.conversation_context or []),
246
+ }
247
  }
248
+
249
+
250
+ @router.post("/enhance/feedback")
251
+ def enhance_feedback(request: FeedbackRequest, user_id: str = Depends(verify_jwt)):
252
+ """Store thumbs up/down feedback on an enhanced prompt."""
253
+ feedback_doc = {
254
+ "user_id": user_id,
255
+ "log_id": request.log_id,
256
+ "rating": request.rating,
257
+ "original": request.original,
258
+ "enhanced": request.enhanced,
259
+ "timestamp": time.time(),
260
+ }
261
+
262
+ if MongoDB.db is not None:
263
+ try:
264
+ # Store in a feedback collection
265
+ MongoDB.db["prompt_feedback"].insert_one(feedback_doc)
266
+ except Exception as e:
267
+ print(f"⚠️ Feedback store error: {e}")
268
+
269
+ return {"status": "recorded", "rating": request.rating}
270
+
271
+
272
+ @router.post("/voice-enhance")
273
+ async def voice_enhance(
274
+ audio: UploadFile = File(...),
275
+ mode: str = Form("deep"),
276
+ platform: str = Form("unknown"),
277
+ conversation_context: str = Form(""),
278
+ selected_prompt_ids: str = Form("[]"),
279
+ user_id: str = Depends(verify_jwt),
280
+ ):
281
+ """
282
+ Voice-to-Prompt pipeline:
283
+ 1. Transcribe audio with Groq Whisper (whisper-large-v3-turbo)
284
+ 2. Enhance transcript with LLM (llama-3.3-70b-versatile)
285
+ 3. Return both transcription and enhanced prompt
286
+ """
287
+ start_time = time.time()
288
+
289
+ # ── 1. READ AUDIO ──
290
+ audio_bytes = await audio.read()
291
+ if len(audio_bytes) < 100:
292
+ return {"error": "Audio too short. Please speak for at least a second."}
293
+
294
+ # ── 2. TRANSCRIBE WITH WHISPER ──
295
+ transcribed_text = ""
296
+ try:
297
+ client = get_groq_client()
298
+ audio_file = io.BytesIO(audio_bytes)
299
+ audio_file.name = audio.filename or "audio.webm"
300
+
301
+ transcription = client.audio.transcriptions.create(
302
+ file=(audio_file.name, audio_file),
303
+ model="whisper-large-v3-turbo",
304
+ language="en",
305
+ response_format="text",
306
+ )
307
+ transcribed_text = transcription.strip() if isinstance(transcription, str) else str(transcription).strip()
308
+ except Exception as e:
309
+ print(f"❌ Whisper transcription error: {e}")
310
+ return {"error": f"Transcription failed: {str(e)}"}
311
+
312
+ if len(transcribed_text) < 3:
313
+ return {"error": "Could not understand audio. Try speaking clearly."}
314
+
315
+ transcription_time = round(time.time() - start_time, 2)
316
+
317
+ # ── 3. ENHANCE THE TRANSCRIPT ──
318
+ # Parse form data
319
+ try:
320
+ ctx_list = json.loads(conversation_context) if conversation_context else []
321
+ except Exception:
322
+ ctx_list = []
323
+ try:
324
+ sel_ids = json.loads(selected_prompt_ids) if selected_prompt_ids else []
325
+ except Exception:
326
+ sel_ids = []
327
+
328
+ # Build an EnhanceRequest and reuse the enhance logic
329
+ enhance_req = EnhanceRequest(
330
+ prompt=transcribed_text,
331
+ mode=mode,
332
+ platform=platform,
333
+ conversation_context=ctx_list if ctx_list else None,
334
+ selected_prompt_ids=sel_ids if sel_ids else None,
335
+ )
336
+ enhance_result = enhance_prompt(enhance_req, user_id)
337
+
338
+ total_time = round(time.time() - start_time, 2)
339
+
340
+ return {
341
+ "transcription": transcribed_text,
342
+ "enhanced": enhance_result.get("enhanced", transcribed_text),
343
+ "original": transcribed_text,
344
+ "mode": mode,
345
+ "transcription_time": transcription_time,
346
+ "total_time": total_time,
347
+ "context_used": enhance_result.get("context_used"),
348
+ "log_id": enhance_result.get("log_id", ""),
349
+ }
350
+
351
+
352
+ def _fetch_saved_prompt(prompt_id: str, user_id: str) -> dict:
353
+ """Helper to get a single saved prompt by ID, owned by user_id."""
354
+ if MongoDB.saved_prompts_col is not None:
355
+ try:
356
+ doc = MongoDB.saved_prompts_col.find_one(
357
+ {"_id": ObjectId(prompt_id), "user_id": user_id}
358
+ )
359
+ return doc
360
+ except Exception:
361
+ return None
362
+ else:
363
+ doc = in_memory_saved_prompts.get(prompt_id)
364
+ if doc and doc.get("user_id") == user_id:
365
+ return doc
366
+ return None
backend/routers/saved_prompts.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import uuid
3
+ from datetime import datetime
4
+ from bson import ObjectId
5
+ from fastapi import APIRouter, Depends, HTTPException
6
+ from ..models.schemas import SavedPromptCreate, SavedPromptUpdate
7
+ from ..core.security import verify_jwt
8
+ from ..core.database import MongoDB, in_memory_saved_prompts
9
+ from ..services.memory_service import MemoryService
10
+
11
+
12
+ def _serialize_dt(val):
13
+ """Safely convert a datetime or string to ISO string."""
14
+ if val is None:
15
+ return None
16
+ if isinstance(val, datetime):
17
+ return val.isoformat()
18
+ return str(val)
19
+
20
+ router = APIRouter(prefix="/saved-prompts", tags=["Saved Prompts"])
21
+
22
+
23
+ @router.post("")
24
+ def create_saved_prompt(body: SavedPromptCreate, user_id: str = Depends(verify_jwt)):
25
+ """Save a prompt to your personal library."""
26
+ doc = {
27
+ "user_id": user_id,
28
+ "content": body.content.strip(),
29
+ "title": (body.title or "").strip() or None,
30
+ "tags": body.tags or [],
31
+ "platform": body.platform or None,
32
+ "created_at": datetime.now(),
33
+ "updated_at": datetime.now(),
34
+ }
35
+
36
+ if MongoDB.saved_prompts_col is not None:
37
+ result = MongoDB.saved_prompts_col.insert_one(doc)
38
+ doc_id = str(result.inserted_id)
39
+ else:
40
+ doc_id = str(uuid.uuid4())
41
+ in_memory_saved_prompts[doc_id] = {**doc, "_id": doc_id}
42
+
43
+ # Embed in Qdrant for similarity search
44
+ MemoryService.embed_saved_prompt(
45
+ user_id=user_id,
46
+ mongo_id=doc_id,
47
+ content=doc["content"],
48
+ title=doc.get("title", ""),
49
+ tags=doc.get("tags", []),
50
+ )
51
+
52
+ return {"id": doc_id, "message": "Prompt saved."}
53
+
54
+
55
+ @router.get("")
56
+ def list_saved_prompts(user_id: str = Depends(verify_jwt)):
57
+ """List all saved prompts for the current user."""
58
+ prompts = []
59
+
60
+ if MongoDB.saved_prompts_col is not None:
61
+ cursor = MongoDB.saved_prompts_col.find(
62
+ {"user_id": user_id}
63
+ ).sort("created_at", -1)
64
+ for doc in cursor:
65
+ prompts.append({
66
+ "id": str(doc["_id"]),
67
+ "content": doc.get("content", ""),
68
+ "title": doc.get("title"),
69
+ "tags": doc.get("tags", []),
70
+ "platform": doc.get("platform"),
71
+ "created_at": _serialize_dt(doc.get("created_at")),
72
+ })
73
+ else:
74
+ for pid, doc in in_memory_saved_prompts.items():
75
+ if doc.get("user_id") == user_id:
76
+ prompts.append({
77
+ "id": pid,
78
+ "content": doc.get("content", ""),
79
+ "title": doc.get("title"),
80
+ "tags": doc.get("tags", []),
81
+ "platform": doc.get("platform"),
82
+ "created_at": _serialize_dt(doc.get("created_at")),
83
+ })
84
+ prompts.sort(key=lambda x: x.get("created_at") or "", reverse=True)
85
+
86
+ return {"prompts": prompts}
87
+
88
+
89
+ @router.put("/{prompt_id}")
90
+ def update_saved_prompt(prompt_id: str, body: SavedPromptUpdate, user_id: str = Depends(verify_jwt)):
91
+ """Update a saved prompt. Re-embeds if content changed."""
92
+ update_fields = {}
93
+ if body.content is not None:
94
+ update_fields["content"] = body.content.strip()
95
+ if body.title is not None:
96
+ update_fields["title"] = body.title.strip() or None
97
+ if body.tags is not None:
98
+ update_fields["tags"] = body.tags
99
+
100
+ if not update_fields:
101
+ raise HTTPException(status_code=400, detail="No fields to update.")
102
+
103
+ update_fields["updated_at"] = datetime.now()
104
+
105
+ if MongoDB.saved_prompts_col is not None:
106
+ result = MongoDB.saved_prompts_col.update_one(
107
+ {"_id": ObjectId(prompt_id), "user_id": user_id},
108
+ {"$set": update_fields}
109
+ )
110
+ if result.matched_count == 0:
111
+ raise HTTPException(status_code=404, detail="Prompt not found.")
112
+
113
+ # If content changed, re-embed
114
+ if "content" in update_fields:
115
+ updated_doc = MongoDB.saved_prompts_col.find_one({"_id": ObjectId(prompt_id)})
116
+ MemoryService.embed_saved_prompt(
117
+ user_id=user_id,
118
+ mongo_id=prompt_id,
119
+ content=updated_doc["content"],
120
+ title=updated_doc.get("title", ""),
121
+ tags=updated_doc.get("tags", []),
122
+ )
123
+ else:
124
+ if prompt_id not in in_memory_saved_prompts:
125
+ raise HTTPException(status_code=404, detail="Prompt not found.")
126
+ doc = in_memory_saved_prompts[prompt_id]
127
+ if doc.get("user_id") != user_id:
128
+ raise HTTPException(status_code=404, detail="Prompt not found.")
129
+ doc.update(update_fields)
130
+ if "content" in update_fields:
131
+ MemoryService.embed_saved_prompt(
132
+ user_id=user_id,
133
+ mongo_id=prompt_id,
134
+ content=doc["content"],
135
+ title=doc.get("title", ""),
136
+ tags=doc.get("tags", []),
137
+ )
138
+
139
+ return {"message": "Prompt updated."}
140
+
141
+
142
+ @router.delete("/{prompt_id}")
143
+ def delete_saved_prompt(prompt_id: str, user_id: str = Depends(verify_jwt)):
144
+ """Delete a saved prompt from Mongo and Qdrant."""
145
+ if MongoDB.saved_prompts_col is not None:
146
+ result = MongoDB.saved_prompts_col.delete_one(
147
+ {"_id": ObjectId(prompt_id), "user_id": user_id}
148
+ )
149
+ if result.deleted_count == 0:
150
+ raise HTTPException(status_code=404, detail="Prompt not found.")
151
+ else:
152
+ if prompt_id not in in_memory_saved_prompts:
153
+ raise HTTPException(status_code=404, detail="Prompt not found.")
154
+ doc = in_memory_saved_prompts[prompt_id]
155
+ if doc.get("user_id") != user_id:
156
+ raise HTTPException(status_code=404, detail="Prompt not found.")
157
+ del in_memory_saved_prompts[prompt_id]
158
+
159
+ MemoryService.delete_saved_prompt_vector(prompt_id)
160
+ return {"message": "Prompt deleted."}
backend/services/memory_service.py CHANGED
@@ -1,22 +1,27 @@
1
 
2
  import time
 
3
  from datetime import datetime
4
- from typing import List, Tuple
5
  from qdrant_client.models import PointStruct, Filter, FieldCondition, MatchValue
6
  from ..core.config import settings
7
- from ..core.database import QdrantDB, MongoDB, in_memory_prompt_logs
8
  from ..services.llm_service import get_embedding
9
 
10
  class MemoryService:
 
 
 
 
 
11
  @staticmethod
12
  def retrieve_context(user_id: str, query_text: str, limit: int = 3) -> Tuple[str, float]:
13
  """
14
- Finds similar past prompts.
15
  Returns: (context_str, max_score)
16
  """
17
  qdrant = QdrantDB.get_client()
18
 
19
- # Default return if DB is down
20
  if qdrant is None:
21
  return "No relevant past context found.", 0.0
22
 
@@ -24,7 +29,6 @@ class MemoryService:
24
  if query_vector is None:
25
  return "No relevant past context found.", 0.0
26
 
27
- # Search with User ID Filter
28
  try:
29
  results = qdrant.search(
30
  collection_name=settings.COLLECTION_NAME,
@@ -51,7 +55,6 @@ class MemoryService:
51
  max_score = hit.score
52
 
53
  payload = hit.payload
54
- # Relevance threshold
55
  if hit.score > 0.25:
56
  context_str += f"- Past Prompt: \"{payload.get('original_prompt')}\"\n"
57
  context_str += f"- Refined Version: \"{payload.get('refined_prompt')}\"\n\n"
@@ -61,10 +64,9 @@ class MemoryService:
61
 
62
  @staticmethod
63
  def get_recent_prompts(user_id: str, limit: int = 5) -> List[str]:
64
- """Fetches most recent prompts."""
65
  recent_prompts = []
66
 
67
- # 1. Try MongoDB
68
  if MongoDB.prompts_col is not None:
69
  try:
70
  cursor = MongoDB.prompts_col.find(
@@ -77,7 +79,6 @@ class MemoryService:
77
  except Exception as e:
78
  print(f"⚠️ Error fetching recent prompts from Mongo: {e}")
79
 
80
- # 2. Fallback to In-Memory
81
  if MongoDB.prompts_col is None:
82
  user_logs = [log for log in in_memory_prompt_logs if log.get("user_id") == user_id]
83
  recent_prompts = [log["original"] for log in user_logs[-limit:]]
@@ -111,7 +112,7 @@ class MemoryService:
111
 
112
  @staticmethod
113
  def memorize_strategy(user_id: str, original: str, refined: str):
114
- """Saves high-quality prompts to Vector DB."""
115
  try:
116
  vec = get_embedding(original)
117
  if vec:
@@ -132,3 +133,100 @@ class MemoryService:
132
  print("💾 New strategy memorized.")
133
  except Exception as e:
134
  print(f"❌ Memorization failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  import time
3
+ import uuid
4
  from datetime import datetime
5
+ from typing import List, Tuple, Optional
6
  from qdrant_client.models import PointStruct, Filter, FieldCondition, MatchValue
7
  from ..core.config import settings
8
+ from ..core.database import QdrantDB, MongoDB, in_memory_prompt_logs, in_memory_saved_prompts
9
  from ..services.llm_service import get_embedding
10
 
11
  class MemoryService:
12
+
13
+ # =========================================================================
14
+ # PASSIVE TRACKING (existing — searches the original prompt_memory collection)
15
+ # =========================================================================
16
+
17
  @staticmethod
18
  def retrieve_context(user_id: str, query_text: str, limit: int = 3) -> Tuple[str, float]:
19
  """
20
+ Finds similar past prompts from PASSIVE tracking.
21
  Returns: (context_str, max_score)
22
  """
23
  qdrant = QdrantDB.get_client()
24
 
 
25
  if qdrant is None:
26
  return "No relevant past context found.", 0.0
27
 
 
29
  if query_vector is None:
30
  return "No relevant past context found.", 0.0
31
 
 
32
  try:
33
  results = qdrant.search(
34
  collection_name=settings.COLLECTION_NAME,
 
55
  max_score = hit.score
56
 
57
  payload = hit.payload
 
58
  if hit.score > 0.25:
59
  context_str += f"- Past Prompt: \"{payload.get('original_prompt')}\"\n"
60
  context_str += f"- Refined Version: \"{payload.get('refined_prompt')}\"\n\n"
 
64
 
65
  @staticmethod
66
  def get_recent_prompts(user_id: str, limit: int = 5) -> List[str]:
67
+ """Fetches most recent prompts from passive log."""
68
  recent_prompts = []
69
 
 
70
  if MongoDB.prompts_col is not None:
71
  try:
72
  cursor = MongoDB.prompts_col.find(
 
79
  except Exception as e:
80
  print(f"⚠️ Error fetching recent prompts from Mongo: {e}")
81
 
 
82
  if MongoDB.prompts_col is None:
83
  user_logs = [log for log in in_memory_prompt_logs if log.get("user_id") == user_id]
84
  recent_prompts = [log["original"] for log in user_logs[-limit:]]
 
112
 
113
  @staticmethod
114
  def memorize_strategy(user_id: str, original: str, refined: str):
115
+ """Saves high-quality prompts to passive tracking Vector DB."""
116
  try:
117
  vec = get_embedding(original)
118
  if vec:
 
133
  print("💾 New strategy memorized.")
134
  except Exception as e:
135
  print(f"❌ Memorization failed: {e}")
136
+
137
+ # =========================================================================
138
+ # SAVED PROMPTS (new — searches the saved_prompt_vectors collection)
139
+ # =========================================================================
140
+
141
+ @staticmethod
142
+ def search_saved_prompts(user_id: str, query_text: str, limit: int = 5, exclude_ids: Optional[List[str]] = None) -> List[dict]:
143
+ """
144
+ Semantic search ONLY against the user's saved prompts.
145
+ Returns list of dicts: [{mongo_id, content, title, tags, score}, ...]
146
+ Excludes any IDs in exclude_ids (already selected by user).
147
+ """
148
+ qdrant = QdrantDB.get_client()
149
+ if qdrant is None:
150
+ return []
151
+
152
+ query_vector = get_embedding(query_text)
153
+ if query_vector is None:
154
+ return []
155
+
156
+ try:
157
+ results = qdrant.search(
158
+ collection_name=QdrantDB.SAVED_COLLECTION,
159
+ query_vector=query_vector,
160
+ query_filter=Filter(
161
+ must=[
162
+ FieldCondition(key="user_id", match=MatchValue(value=user_id))
163
+ ]
164
+ ),
165
+ limit=limit + (len(exclude_ids) if exclude_ids else 0),
166
+ )
167
+ except Exception as e:
168
+ print(f"⚠️ Saved prompts search failed: {e}")
169
+ return []
170
+
171
+ exclude_set = set(exclude_ids or [])
172
+ matched = []
173
+ for hit in results:
174
+ mongo_id = hit.payload.get("mongo_id", "")
175
+ if mongo_id in exclude_set:
176
+ continue
177
+ if hit.score < 0.20:
178
+ continue
179
+ matched.append({
180
+ "mongo_id": mongo_id,
181
+ "content": hit.payload.get("content", ""),
182
+ "title": hit.payload.get("title", ""),
183
+ "tags": hit.payload.get("tags", []),
184
+ "score": round(hit.score, 3),
185
+ })
186
+ if len(matched) >= limit:
187
+ break
188
+
189
+ return matched
190
+
191
+ @staticmethod
192
+ def embed_saved_prompt(user_id: str, mongo_id: str, content: str, title: str = "", tags: list = None):
193
+ """Embed a saved prompt into the saved_prompt_vectors Qdrant collection."""
194
+ try:
195
+ vec = get_embedding(content)
196
+ if vec:
197
+ q_client = QdrantDB.get_client()
198
+ if q_client:
199
+ # Use a deterministic numeric ID from the mongo_id hash
200
+ point_id = abs(hash(mongo_id)) % (2**63)
201
+ q_client.upsert(
202
+ collection_name=QdrantDB.SAVED_COLLECTION,
203
+ points=[PointStruct(
204
+ id=point_id,
205
+ vector=vec,
206
+ payload={
207
+ "user_id": user_id,
208
+ "mongo_id": mongo_id,
209
+ "content": content,
210
+ "title": title or "",
211
+ "tags": tags or [],
212
+ }
213
+ )]
214
+ )
215
+ print(f"💾 Saved prompt embedded (id={mongo_id})")
216
+ except Exception as e:
217
+ print(f"❌ Saved prompt embedding failed: {e}")
218
+
219
+ @staticmethod
220
+ def delete_saved_prompt_vector(mongo_id: str):
221
+ """Remove a saved prompt's vector from Qdrant."""
222
+ try:
223
+ q_client = QdrantDB.get_client()
224
+ if q_client:
225
+ point_id = abs(hash(mongo_id)) % (2**63)
226
+ q_client.delete(
227
+ collection_name=QdrantDB.SAVED_COLLECTION,
228
+ points_selector=[point_id],
229
+ )
230
+ print(f"🗑️ Saved prompt vector deleted (id={mongo_id})")
231
+ except Exception as e:
232
+ print(f"⚠️ Could not delete saved prompt vector: {e}")