Spaces:
Running
Running
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 +1 -2
- backend/core/database.py +44 -32
- backend/main.py +2 -1
- backend/models/schemas.py +38 -0
- backend/requirements.txt +4 -1
- backend/routers/auth.py +8 -2
- backend/routers/prompts.py +295 -71
- backend/routers/saved_prompts.py +160 -0
- backend/services/memory_service.py +108 -10
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 = "
|
| 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 |
-
#
|
| 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 =
|
| 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":
|
| 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 |
-
|
| 4 |
-
from
|
|
|
|
|
|
|
| 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 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
-
|
| 37 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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:
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
# 1.
|
| 68 |
user_data = None
|
| 69 |
if MongoDB.users_col is not None:
|
| 70 |
-
user_data = MongoDB.users_col.find_one({"user_id":
|
| 71 |
if user_data is None:
|
| 72 |
-
user_data = in_memory_users.get(
|
| 73 |
|
| 74 |
-
ts_raw = user_data.get("tech_stack", [
|
| 75 |
tech_stack = ", ".join(ts_raw) if isinstance(ts_raw, list) else str(ts_raw)
|
| 76 |
-
preferences = user_data.get("preferences", "
|
| 77 |
|
| 78 |
-
# 2.
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
# 3.
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
# 4.
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
#
|
| 102 |
-
|
| 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":
|
| 111 |
{"role": "user", "content": user_message}
|
| 112 |
],
|
| 113 |
-
model="
|
| 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 |
-
#
|
|
|
|
| 123 |
log_id = MemoryService.log_prompt(
|
| 124 |
-
user_id=
|
| 125 |
original=request.prompt,
|
| 126 |
enhanced=enhanced_prompt,
|
| 127 |
score=max_similarity,
|
| 128 |
latency=process_time,
|
| 129 |
)
|
| 130 |
|
| 131 |
-
#
|
| 132 |
if max_similarity < 0.90:
|
| 133 |
-
MemoryService.memorize_strategy(
|
| 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}")
|