Hammad712 commited on
Commit
4c94669
·
1 Parent(s): 822f946

Updated vectorstore

Browse files
app/main.py CHANGED
@@ -119,6 +119,24 @@ async def health_check():
119
  uptime=uptime_str
120
  )
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  @app.exception_handler(404)
123
  async def not_found_handler(request, exc):
124
  logger.warning("404 Not Found: %s %s", request.method, request.url.path)
 
119
  uptime=uptime_str
120
  )
121
 
122
+ # from app.rag.utils import cleanup_old_vectorstores
123
+
124
+ # @asynccontextmanager
125
+ # async def lifespan(app: FastAPI):
126
+ # global startup_time
127
+ # startup_time = time.time()
128
+ # logger.info("🚀 Starting %s v%s", settings.app_name, settings.app_version)
129
+ # logger.info("📊 Server will run on %s:%s", settings.host, settings.port)
130
+
131
+ # # Trigger cleanup on startup
132
+ # deleted = cleanup_old_vectorstores(days=7)
133
+ # logger.info("🧹 Cleanup complete. %s old sessions removed.", deleted)
134
+
135
+ # yield
136
+
137
+ # logger.info("📊 Shutting down %s", settings.app_name)
138
+
139
+
140
  @app.exception_handler(404)
141
  async def not_found_handler(request, exc):
142
  logger.warning("404 Not Found: %s %s", request.method, request.url.path)
app/mobile_usability/prompts.py CHANGED
@@ -31,6 +31,10 @@ Formatting rules:
31
  - Each list may contain zero or more items, but critical items must appear in "high".
32
  - Ensure items are specific enough for a developer to action (mention affected selector(s) when possible).
33
 
 
 
 
 
34
  {format_instructions}
35
 
36
  Use the following to guide prioritization:
 
31
  - Each list may contain zero or more items, but critical items must appear in "high".
32
  - Ensure items are specific enough for a developer to action (mention affected selector(s) when possible).
33
 
34
+ Important:
35
+ - Respond with *only* a valid JSON object.
36
+ - Do NOT include any commentary or explanation outside the JSON.
37
+
38
  {format_instructions}
39
 
40
  Use the following to guide prioritization:
app/page_speed/config.py CHANGED
@@ -12,6 +12,11 @@ class Settings(BaseSettings):
12
  pagespeed_api_key: str
13
  gemini_api_key: str
14
 
 
 
 
 
 
15
 
16
  # ───────────────────────────────────────────────────────────────────────────
17
  # Chat & RAG Configuration
@@ -30,18 +35,18 @@ class Settings(BaseSettings):
30
 
31
  @property
32
  def mongo_uri(self) -> str:
33
- pw = quote_plus(self.mongo_password)
34
- return (
35
- f"mongodb+srv://{self.mongo_user}:{pw}@{self.mongo_host}/"
36
- f"{self.mongo_db}?retryWrites=true&w=majority&ssl=true"
37
- )
38
 
39
 
40
  # ───────────────────────────────────────────────────────────────────────────
41
  # local MongoDB Connection
42
  # ───────────────────────────────────────────────────────────────────────────
43
 
44
- #return f"mongodb://localhost:27017/{self.mongo_db}"
45
 
46
 
47
  # ───────────────────────────────────────────────────────────────────────────
 
12
  pagespeed_api_key: str
13
  gemini_api_key: str
14
 
15
+ # Qdrant (vector DB) connection (optional; if not set, QdrantClient will use defaults)
16
+ qdrant_url: str
17
+ qdrant_api_key: str
18
+ # Optional timeout (seconds) to use when creating clients or making calls
19
+ qdrant_timeout: int = 60
20
 
21
  # ───────────────────────────────────────────────────────────────────────────
22
  # Chat & RAG Configuration
 
35
 
36
  @property
37
  def mongo_uri(self) -> str:
38
+ # pw = quote_plus(self.mongo_password)
39
+ # return (
40
+ # f"mongodb+srv://{self.mongo_user}:{pw}@{self.mongo_host}/"
41
+ # f"{self.mongo_db}?retryWrites=true&w=majority&ssl=true"
42
+ # )
43
 
44
 
45
  # ───────────────────────────────────────────────────────────────────────────
46
  # local MongoDB Connection
47
  # ───────────────────────────────────────────────────────────────────────────
48
 
49
+ return f"mongodb://localhost:27017/{self.mongo_db}"
50
 
51
 
52
  # ───────────────────────────────────────────────────────────────────────────
app/rag/chat_history.py CHANGED
@@ -1,12 +1,10 @@
1
- import os
2
  import time
3
  from typing import List, Dict, Any
4
  from pymongo import ReturnDocument
5
 
6
  from app.page_speed.config import settings
7
- from .db import mongo_client, chat_collection_name
8
  from .embeddings import get_llm
9
- from .utils import get_vectorstore_path # make sure this util is available
10
  from langchain.prompts import ChatPromptTemplate
11
  from .logging_config import logger
12
 
@@ -21,6 +19,7 @@ summarization_prompt = ChatPromptTemplate.from_messages([
21
  ("human", "{chat_history}")
22
  ])
23
 
 
24
  class ChatHistoryManager:
25
  @staticmethod
26
  def create_session(chat_id: str) -> None:
@@ -82,13 +81,13 @@ class ChatHistoryManager:
82
  return True
83
 
84
  @staticmethod
85
- def vectorstore_exists(user_id: str) -> bool:
86
  """
87
- Check if a vectorstore directory already exists for this user.
88
  """
89
- path = get_vectorstore_path(user_id)
90
- exists = os.path.isdir(path)
91
- logger.debug("Vectorstore path %s exists: %s", path, exists)
92
  return exists
93
 
94
  @staticmethod
 
 
1
  import time
2
  from typing import List, Dict, Any
3
  from pymongo import ReturnDocument
4
 
5
  from app.page_speed.config import settings
6
+ from .db import mongo_client, chat_collection_name, qdrant_client
7
  from .embeddings import get_llm
 
8
  from langchain.prompts import ChatPromptTemplate
9
  from .logging_config import logger
10
 
 
19
  ("human", "{chat_history}")
20
  ])
21
 
22
+
23
  class ChatHistoryManager:
24
  @staticmethod
25
  def create_session(chat_id: str) -> None:
 
81
  return True
82
 
83
  @staticmethod
84
+ def vectorstore_exists(collection_name: str) -> bool:
85
  """
86
+ Check if a Qdrant collection exists instead of local FAISS path.
87
  """
88
+ collections = qdrant_client.get_collections().collections
89
+ exists = any(c.name == collection_name for c in collections)
90
+ logger.debug("Qdrant collection %s exists: %s", collection_name, exists)
91
  return exists
92
 
93
  @staticmethod
app/rag/db.py CHANGED
@@ -1,38 +1,57 @@
1
- # db.py
2
- from pymongo import MongoClient
3
- from app.page_speed.config import settings
 
4
 
5
- # ──────────────────────────────────────────────────────────────────────────────
6
- # MongoDB Initialization
7
- # ──────────────────────────────────────────────────────────────────────────────
8
 
9
- # Connect to MongoDB using the URI from settings
10
- mongo_client = MongoClient(settings.mongo_uri)
11
 
12
- # Use the renamed settings attributes
13
- mongo_db = mongo_client[settings.mongo_db]
14
 
15
- # Collection to store metadata that maps user_id → vectorstore_path
16
- vectorstore_meta_coll = mongo_db["vectorstore_metadata"]
17
 
18
- # Name of the collection that MongoDBChatMessageHistory will write to
19
- chat_collection_name = settings.mongo_collection
20
 
 
 
 
 
 
 
 
 
21
 
22
- # # ____________________________________________________________
23
- # #Local MongoDB Connection
24
- # # ____________________________________________________________
25
 
26
- # # db.py
27
- # from pymongo import MongoClient
28
- # from app.page_speed.config import settings
 
29
 
30
- # # Always connect to local MongoDB
31
- # mongo_client = MongoClient("mongodb://localhost:27017/")
32
 
33
- # # Select the database from settings
34
- # mongo_db = mongo_client[settings.mongo_db]
35
 
36
- # # Collections
37
- # vectorstore_meta_coll = mongo_db["vectorstore_metadata"]
38
- # chat_collection_name = settings.mongo_collection
 
 
 
 
 
 
 
 
 
 
1
+ # # db.py
2
+ # from pymongo import MongoClient
3
+ # from app.page_speed.config import settings
4
+ #from qdrant_client import QdrantClient
5
 
6
+ # # ──────────────────────────────────────────────────────────────────────────────
7
+ # # MongoDB Initialization
8
+ # # ──────────────────────────────────────────────────────────────────────────────
9
 
10
+ # # Connect to MongoDB using the URI from settings
11
+ # mongo_client = MongoClient(settings.mongo_uri)
12
 
13
+ # # Use the renamed settings attributes
14
+ # mongo_db = mongo_client[settings.mongo_db]
15
 
16
+ # # Collection to store metadata that maps user_id → vectorstore_path
17
+ # vectorstore_meta_coll = mongo_db["vectorstore_metadata"]
18
 
19
+ # # Name of the collection that MongoDBChatMessageHistory will write to
20
+ # chat_collection_name = settings.mongo_collection
21
 
22
+ # # ─────────────────────────────────────────────
23
+ # # Qdrant Setup
24
+ # # ─────────────────────────────────────────────
25
+ # # If Qdrant is running locally
26
+ # qdrant_client = QdrantClient(
27
+ # url=settings.qdrant_url, # e.g. "http://localhost:6333"
28
+ # api_key=settings.qdrant_api_key or None
29
+ # )
30
 
31
+ # ____________________________________________________________
32
+ #Local MongoDB Connection
33
+ # ____________________________________________________________
34
 
35
+ # db.py
36
+ from pymongo import MongoClient
37
+ from app.page_speed.config import settings
38
+ from qdrant_client import QdrantClient
39
 
40
+ # Always connect to local MongoDB
41
+ mongo_client = MongoClient("mongodb://localhost:27017/")
42
 
43
+ # Select the database from settings
44
+ mongo_db = mongo_client[settings.mongo_db]
45
 
46
+ # Collections
47
+ vectorstore_meta_coll = mongo_db["vectorstore_metadata"]
48
+ chat_collection_name = settings.mongo_collection
49
+
50
+ # ─────────────────────────────────────────────
51
+ # Qdrant Setup
52
+ # ─────────────────────────────────────────────
53
+ # If Qdrant is running locally
54
+ qdrant_client = QdrantClient(
55
+ url=settings.qdrant_url, # e.g. "http://localhost:6333"
56
+ api_key=settings.qdrant_api_key or None
57
+ )
app/rag/routes.py CHANGED
@@ -1,12 +1,16 @@
 
 
1
  import os
 
2
  import uuid
 
 
 
3
  from fastapi import APIRouter, HTTPException, Path, Query
4
 
5
  from .schemas import SetupRequest, ChatRequest, SetupResponse, ChatResponse
6
  from .utils import (
7
  get_vectorstore_path,
8
- text_splitter,
9
- embeddings,
10
  save_vectorstore_to_disk,
11
  upsert_vectorstore_metadata,
12
  get_vectorstore_metadata,
@@ -15,8 +19,49 @@ from .utils import (
15
  from .chat_history import ChatHistoryManager
16
  from .logging_config import logger
17
 
 
 
 
 
 
18
  router = APIRouter(prefix="/rag", tags=["rag"])
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  @router.post("/initialization/{onboarding_id}/{doc_type}", response_model=SetupResponse)
21
  async def setup_rag_session(
22
  onboarding_id: str = Path(..., description="Unique onboarding identifier"),
@@ -25,34 +70,35 @@ async def setup_rag_session(
25
  ):
26
  """
27
  Ingest documents under a specific document type and create a chat session.
28
- - If vectorstore exists for onboarding_id and doc_type, skip ingestion.
29
  - Always create a new chat_id for this session.
 
30
  """
31
- vectorstore_path = get_vectorstore_path(onboarding_id, doc_type)
32
-
33
- # Existing vectorstore
34
- if os.path.isdir(os.path.join(vectorstore_path, "faiss_index")):
35
  logger.info(
36
- "Vectorstore exists for onboarding_id=%s, doc_type=%s; skipping ingestion",
37
  onboarding_id, doc_type
38
  )
39
- metadata = get_vectorstore_metadata(onboarding_id, doc_type)
40
  if metadata and metadata.get("chat_id"):
41
  chat_id = metadata["chat_id"]
42
  else:
43
  chat_id = str(uuid.uuid4())
44
  ChatHistoryManager.create_session(chat_id)
45
- upsert_vectorstore_metadata(onboarding_id, doc_type, vectorstore_path, chat_id)
 
46
  return SetupResponse(
47
  success=True,
48
- message="RAG setup completed with existing vectorstore.",
49
  onboarding_id=onboarding_id,
50
  doc_type=doc_type,
51
  chat_id=chat_id,
52
- vectorstore_path=vectorstore_path
53
  )
54
 
55
- # New ingestion
56
  if not body.documents:
57
  logger.error(
58
  "Missing documents for onboarding_id=%s, doc_type=%s",
@@ -63,14 +109,123 @@ async def setup_rag_session(
63
  # Create session and ingest
64
  chat_id = str(uuid.uuid4())
65
  ChatHistoryManager.create_session(chat_id)
 
66
  all_text = "\n\n".join(body.documents)
67
  text_chunks = text_splitter.split_text(all_text)
68
- vs = __import__("langchain_community.vectorstores").vectorstores.FAISS.from_texts(
69
- texts=text_chunks,
70
- embedding=embeddings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  )
72
- vs_path = save_vectorstore_to_disk(vs, onboarding_id, doc_type)
73
- upsert_vectorstore_metadata(onboarding_id, doc_type, vs_path, chat_id)
74
 
75
  return SetupResponse(
76
  success=True,
@@ -81,6 +236,7 @@ async def setup_rag_session(
81
  vectorstore_path=vs_path
82
  )
83
 
 
84
  @router.post("/chat/{onboarding_id}/{doc_type}/{chat_id}", response_model=ChatResponse)
85
  async def chat_with_user(
86
  onboarding_id: str = Path(...),
@@ -92,21 +248,25 @@ async def chat_with_user(
92
  """
93
  Chat endpoint using a specific document-type vectorstore.
94
  """
95
- vectorstore_path = get_vectorstore_path(onboarding_id, doc_type)
96
- if not os.path.isdir(os.path.join(vectorstore_path, "faiss_index")):
97
- raise HTTPException(status_code=400, detail="Vectorstore not found; run initialization first.")
 
98
 
99
  if not ChatHistoryManager.chat_exists(chat_id):
100
  raise HTTPException(status_code=404, detail=f"Chat session {chat_id} not found.")
101
 
102
- question = body.question.strip()
 
 
 
103
  ChatHistoryManager.summarize_if_needed(chat_id, threshold=10)
104
  ChatHistoryManager.add_message(chat_id, role="human", content=question)
105
 
106
  chain = build_rag_chain(onboarding_id, doc_type, chat_id, prompt_type)
107
  history = ChatHistoryManager.get_messages(chat_id)
108
  result = chain.invoke({"question": question, "chat_history": history})
109
- answer = result.get("answer") or result.get("output_text")
110
  ChatHistoryManager.add_message(chat_id, role="ai", content=answer)
111
 
112
  return ChatResponse(
@@ -116,4 +276,4 @@ async def chat_with_user(
116
  chat_id=chat_id,
117
  onboarding_id=onboarding_id,
118
  doc_type=doc_type
119
- )
 
1
+ # app/rag/routes.py
2
+
3
  import os
4
+ import json
5
  import uuid
6
+ import time
7
+ from typing import List, Optional, Iterable
8
+
9
  from fastapi import APIRouter, HTTPException, Path, Query
10
 
11
  from .schemas import SetupRequest, ChatRequest, SetupResponse, ChatResponse
12
  from .utils import (
13
  get_vectorstore_path,
 
 
14
  save_vectorstore_to_disk,
15
  upsert_vectorstore_metadata,
16
  get_vectorstore_metadata,
 
19
  from .chat_history import ChatHistoryManager
20
  from .logging_config import logger
21
 
22
+ from qdrant_client import QdrantClient
23
+ from qdrant_client.models import VectorParams, PointStruct, Distance
24
+ from app.page_speed.config import settings
25
+ from .embeddings import embeddings, text_splitter # kept here for ingestion
26
+
27
  router = APIRouter(prefix="/rag", tags=["rag"])
28
 
29
+
30
+ def _get_embeddings_for_texts(texts: List[str]) -> List[List[float]]:
31
+ """
32
+ Try common embedding API names (embed_documents, embed_texts, embed).
33
+ Falls back to calling embed_query per text (slower).
34
+ """
35
+ if not texts:
36
+ return []
37
+
38
+ # Preferred bulk API
39
+ for attr in ("embed_documents", "embed_texts", "embed_batch", "embed"):
40
+ fn = getattr(embeddings, attr, None)
41
+ if callable(fn):
42
+ try:
43
+ return fn(texts)
44
+ except Exception:
45
+ logger.debug("Embedding method %s failed; trying next option", attr, exc_info=True)
46
+
47
+ # Fallback: try single-item embedding function repeatedly
48
+ single_fn = getattr(embeddings, "embed_query", None) or getattr(embeddings, "embed", None)
49
+ if callable(single_fn):
50
+ vecs = []
51
+ for t in texts:
52
+ vec = single_fn(t)
53
+ if isinstance(vec, dict) and "embedding" in vec:
54
+ vecs.append(vec["embedding"])
55
+ else:
56
+ vecs.append(vec)
57
+ return vecs
58
+
59
+ raise RuntimeError(
60
+ "Embeddings object does not expose a supported embedding method "
61
+ "(embed_documents/embed_texts/embed_query)."
62
+ )
63
+
64
+
65
  @router.post("/initialization/{onboarding_id}/{doc_type}", response_model=SetupResponse)
66
  async def setup_rag_session(
67
  onboarding_id: str = Path(..., description="Unique onboarding identifier"),
 
70
  ):
71
  """
72
  Ingest documents under a specific document type and create a chat session.
73
+ - If vectorstore metadata exists for onboarding_id and doc_type in MongoDB, skip ingestion.
74
  - Always create a new chat_id for this session.
75
+ NOTE: This implementation does NOT create or rely on any local files on disk for metadata.
76
  """
77
+ # Use DB metadata instead of local filesystem marker
78
+ existing_meta = get_vectorstore_metadata(onboarding_id, doc_type)
79
+ if existing_meta:
 
80
  logger.info(
81
+ "Vectorstore metadata exists for onboarding_id=%s, doc_type=%s; skipping ingestion",
82
  onboarding_id, doc_type
83
  )
84
+ metadata = existing_meta
85
  if metadata and metadata.get("chat_id"):
86
  chat_id = metadata["chat_id"]
87
  else:
88
  chat_id = str(uuid.uuid4())
89
  ChatHistoryManager.create_session(chat_id)
90
+ # ensure DB has chat_id
91
+ upsert_vectorstore_metadata(onboarding_id, doc_type, metadata.get("vectorstore_path"), chat_id, metadata.get("collection_name"))
92
  return SetupResponse(
93
  success=True,
94
+ message="RAG setup completed with existing vectorstore metadata.",
95
  onboarding_id=onboarding_id,
96
  doc_type=doc_type,
97
  chat_id=chat_id,
98
+ vectorstore_path=metadata.get("vectorstore_path")
99
  )
100
 
101
+ # New ingestion flow
102
  if not body.documents:
103
  logger.error(
104
  "Missing documents for onboarding_id=%s, doc_type=%s",
 
109
  # Create session and ingest
110
  chat_id = str(uuid.uuid4())
111
  ChatHistoryManager.create_session(chat_id)
112
+
113
  all_text = "\n\n".join(body.documents)
114
  text_chunks = text_splitter.split_text(all_text)
115
+
116
+ # Build Qdrant client from settings (with timeout + optional prefer_grpc)
117
+ client_kwargs = {}
118
+ if getattr(settings, "qdrant_url", None):
119
+ client_kwargs["url"] = settings.qdrant_url
120
+ if getattr(settings, "qdrant_api_key", None):
121
+ client_kwargs["api_key"] = settings.qdrant_api_key
122
+
123
+ # sensible defaults; override via app config
124
+ qdrant_timeout = getattr(settings, "qdrant_timeout", 60) # seconds (default 60)
125
+ prefer_grpc = getattr(settings, "qdrant_prefer_grpc", False) # set True to use gRPC if available
126
+
127
+ try:
128
+ if client_kwargs:
129
+ qdrant_client = QdrantClient(**client_kwargs, timeout=qdrant_timeout, prefer_grpc=prefer_grpc)
130
+ else:
131
+ qdrant_client = QdrantClient(timeout=qdrant_timeout, prefer_grpc=prefer_grpc)
132
+ except TypeError as e:
133
+ logger.exception("Failed to instantiate QdrantClient: %s", e)
134
+ raise HTTPException(status_code=500, detail=f"Failed to construct Qdrant client: {e}")
135
+
136
+ # Deterministic collection name for each onboarding/doc_type
137
+ collection_name = f"vs_{onboarding_id}_{doc_type}"
138
+
139
+ # --------------------------
140
+ # INGEST: compute embeddings
141
+ # --------------------------
142
+ try:
143
+ vectors = _get_embeddings_for_texts(text_chunks)
144
+ except Exception as e:
145
+ logger.exception("Failed to compute embeddings: %s", e)
146
+ raise HTTPException(status_code=500, detail=f"Embedding error: {e}")
147
+
148
+ if not vectors or len(vectors) != len(text_chunks):
149
+ logger.error("Embeddings length mismatch: vectors=%s texts=%s", len(vectors), len(text_chunks))
150
+ raise HTTPException(status_code=500, detail="Embedding generation failed or returned unexpected shape.")
151
+
152
+ vector_size = len(vectors[0])
153
+ if vector_size == 0:
154
+ raise HTTPException(status_code=500, detail="Embedding returned empty vectors")
155
+
156
+ # Recreate collection (idempotent for onboarding+doc_type)
157
+ try:
158
+ qdrant_client.recreate_collection(
159
+ collection_name=collection_name,
160
+ vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE)
161
+ )
162
+ except Exception as e:
163
+ logger.exception("Failed to create/recreate qdrant collection '%s': %s", collection_name, e)
164
+ raise HTTPException(status_code=500, detail=f"Failed to create qdrant collection: {e}")
165
+
166
+ # Helper: safe upsert with retries/backoff
167
+ def safe_upsert(client: QdrantClient, collection_name: str, points: Iterable[PointStruct], max_retries: int = 3):
168
+ attempt = 0
169
+ backoff = 1.0
170
+ last_exc: Optional[Exception] = None
171
+ while attempt < max_retries:
172
+ try:
173
+ client.upsert(collection_name=collection_name, points=points)
174
+ return
175
+ except Exception as exc:
176
+ last_exc = exc
177
+ attempt += 1
178
+ logger.warning("Qdrant upsert attempt %d/%d failed: %s", attempt, max_retries, exc)
179
+ if attempt >= max_retries:
180
+ logger.exception("Qdrant upsert failed after %d attempts", max_retries)
181
+ raise
182
+ # exponential backoff
183
+ time.sleep(backoff)
184
+ backoff *= 2.0
185
+ # if loop finishes without returning, raise last exception
186
+ if last_exc:
187
+ raise last_exc
188
+
189
+ # Upsert points in smaller batches and use safe_upsert
190
+ batch_size = getattr(settings, "qdrant_upsert_batch_size", 64) # smaller default batch size
191
+ points_batch: List[PointStruct] = []
192
+ try:
193
+ for i, (vec, txt) in enumerate(zip(vectors, text_chunks)):
194
+ payload = {"text": txt}
195
+ # Use UUID string for id to avoid collisions across sessions
196
+ point_id = str(uuid.uuid4())
197
+ point = PointStruct(id=point_id, vector=vec, payload=payload)
198
+ points_batch.append(point)
199
+
200
+ if len(points_batch) >= batch_size:
201
+ logger.debug("Upserting batch of %d points to collection %s", len(points_batch), collection_name)
202
+ safe_upsert(qdrant_client, collection_name, points_batch)
203
+ points_batch = []
204
+
205
+ # final flush
206
+ if points_batch:
207
+ logger.debug("Upserting final batch of %d points to collection %s", len(points_batch), collection_name)
208
+ safe_upsert(qdrant_client, collection_name, points_batch)
209
+ except Exception as e:
210
+ logger.exception("Failed to upsert points into qdrant: %s", e)
211
+ raise HTTPException(status_code=500, detail=f"Failed to upsert points into Qdrant: {e}")
212
+
213
+ # Create an in-application "vectorstore_path" (URI-style) and store metadata in DB
214
+ vs_path = save_vectorstore_to_disk(
215
+ onboarding_id,
216
+ doc_type,
217
+ collection_name,
218
+ getattr(settings, "qdrant_url", None),
219
+ getattr(settings, "qdrant_api_key", None)
220
+ )
221
+ # Persist metadata into MongoDB (no local disk involved)
222
+ # Persist extra metadata fields so retrieval can use same connection details (if desired)
223
+ upsert_vectorstore_metadata(onboarding_id, doc_type, vs_path, chat_id, collection_name)
224
+
225
+ logger.info(
226
+ "Created Qdrant collection %s for %s/%s (points=%d)",
227
+ collection_name, onboarding_id, doc_type, len(text_chunks)
228
  )
 
 
229
 
230
  return SetupResponse(
231
  success=True,
 
236
  vectorstore_path=vs_path
237
  )
238
 
239
+
240
  @router.post("/chat/{onboarding_id}/{doc_type}/{chat_id}", response_model=ChatResponse)
241
  async def chat_with_user(
242
  onboarding_id: str = Path(...),
 
248
  """
249
  Chat endpoint using a specific document-type vectorstore.
250
  """
251
+ # Use DB metadata instead of local filesystem marker
252
+ metadata = get_vectorstore_metadata(onboarding_id, doc_type)
253
+ if not metadata:
254
+ raise HTTPException(status_code=400, detail="Vectorstore metadata not found; run initialization first.")
255
 
256
  if not ChatHistoryManager.chat_exists(chat_id):
257
  raise HTTPException(status_code=404, detail=f"Chat session {chat_id} not found.")
258
 
259
+ question = (body.question or "").strip()
260
+ if not question:
261
+ raise HTTPException(status_code=400, detail="Question cannot be empty.")
262
+
263
  ChatHistoryManager.summarize_if_needed(chat_id, threshold=10)
264
  ChatHistoryManager.add_message(chat_id, role="human", content=question)
265
 
266
  chain = build_rag_chain(onboarding_id, doc_type, chat_id, prompt_type)
267
  history = ChatHistoryManager.get_messages(chat_id)
268
  result = chain.invoke({"question": question, "chat_history": history})
269
+ answer = result.get("answer") or result.get("output_text") or ""
270
  ChatHistoryManager.add_message(chat_id, role="ai", content=answer)
271
 
272
  return ChatResponse(
 
276
  chat_id=chat_id,
277
  onboarding_id=onboarding_id,
278
  doc_type=doc_type
279
+ )
app/rag/utils.py CHANGED
@@ -1,11 +1,22 @@
 
 
1
  import os
2
- from typing import Optional, Dict, Any
 
 
 
3
  from fastapi import HTTPException
4
 
5
- from langchain_community.vectorstores import FAISS
 
 
6
  from langchain_mongodb.chat_message_histories import MongoDBChatMessageHistory
7
  from langchain.memory import ConversationBufferMemory
8
  from langchain.chains import ConversationalRetrievalChain
 
 
 
 
9
 
10
  from app.page_speed.config import settings
11
  from .db import vectorstore_meta_coll, chat_collection_name
@@ -20,46 +31,160 @@ from .prompt_library import (
20
  mobile_usability_prompt
21
  )
22
 
23
- # 1. Path with doc_type
 
 
 
 
24
  def get_vectorstore_path(onboarding_id: str, doc_type: str) -> str:
25
  """
26
- Returns './vectorstores/{onboarding_id}/{doc_type}'.
 
 
 
27
  """
28
- base_dir = settings.vectorstore_base_path
29
- return os.path.join(base_dir, onboarding_id, doc_type)
30
 
31
- # 2. Save to disk under doc_type
32
- def save_vectorstore_to_disk(vectorstore: FAISS, onboarding_id: str, doc_type: str) -> str:
 
 
 
 
 
33
  """
34
- Save under './vectorstores/{onboarding_id}/{doc_type}/faiss_index'.
 
 
35
  """
36
- vs_dir = get_vectorstore_path(onboarding_id, doc_type)
37
- faiss_index_path = os.path.join(vs_dir, "faiss_index")
38
- os.makedirs(faiss_index_path, exist_ok=True)
39
- vectorstore.save_local(folder_path=faiss_index_path)
40
- return faiss_index_path
41
 
42
- # 3. Metadata now includes doc_type
43
  def upsert_vectorstore_metadata(
44
  onboarding_id: str,
45
  doc_type: str,
46
  vectorstore_path: str,
47
- chat_id: str
 
 
 
48
  ) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  vectorstore_meta_coll.update_one(
50
  {"onboarding_id": onboarding_id, "doc_type": doc_type},
51
- {"$set": {"vectorstore_path": vectorstore_path, "chat_id": chat_id}},
52
  upsert=True
53
  )
 
54
 
55
 
56
  def get_vectorstore_metadata(
57
  onboarding_id: str,
58
  doc_type: str
59
  ) -> Optional[Dict[str, Any]]:
60
- return vectorstore_meta_coll.find_one({"onboarding_id": onboarding_id, "doc_type": doc_type})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- # 4. Build chain now takes doc_type
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  def build_rag_chain(
65
  onboarding_id: str,
@@ -67,14 +192,112 @@ def build_rag_chain(
67
  chat_id: str,
68
  prompt_type: str
69
  ) -> ConversationalRetrievalChain:
70
- # Load index
71
- vs_path = get_vectorstore_path(onboarding_id, doc_type)
72
- faiss_vs = FAISS.load_local(
73
- folder_path=os.path.join(vs_path, "faiss_index"),
74
- embeddings=embeddings,
75
- allow_dangerous_deserialization=True
76
- )
77
- retriever = faiss_vs.as_retriever(search_kwargs={"k": 5})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # History & memory
80
  chat_history = MongoDBChatMessageHistory(
@@ -85,10 +308,12 @@ def build_rag_chain(
85
  )
86
  memory = ConversationBufferMemory(
87
  memory_key="chat_history",
88
- chat_history=chat_history
 
89
  )
90
 
91
  llm = get_llm()
 
92
  # Choose prompt
93
  if prompt_type == "page_speed":
94
  user_prompt = page_speed_prompt
 
1
+ # app/rag/utils.py
2
+
3
  import os
4
+ import json
5
+ from typing import Optional, Dict, Any, List
6
+ from datetime import datetime
7
+
8
  from fastapi import HTTPException
9
 
10
+ from qdrant_client import QdrantClient
11
+ from qdrant_client.http import models as qdrant_models
12
+
13
  from langchain_mongodb.chat_message_histories import MongoDBChatMessageHistory
14
  from langchain.memory import ConversationBufferMemory
15
  from langchain.chains import ConversationalRetrievalChain
16
+ from langchain_core.retrievers import BaseRetriever
17
+ from langchain_core.documents import Document
18
+
19
+ from pydantic import ConfigDict # Pydantic v2 config for BaseModel-based classes
20
 
21
  from app.page_speed.config import settings
22
  from .db import vectorstore_meta_coll, chat_collection_name
 
31
  mobile_usability_prompt
32
  )
33
 
34
+
35
+ # ──────────────────────────────────────────────────────────────────────────────
36
+ # Paths & metadata helpers (diskless)
37
+ # ──────────────────────────────────────────────────────────────────────────────
38
+
39
  def get_vectorstore_path(onboarding_id: str, doc_type: str) -> str:
40
  """
41
+ Returns a non-disk URI-like path for a vectorstore.
42
+ Example: 'qdrant://<onboarding_id>/<doc_type>'
43
+ This avoids creating a local folder while preserving a string that identifies
44
+ the logical vectorstore for other components and logs.
45
  """
46
+ return f"qdrant://{onboarding_id}/{doc_type}"
47
+
48
 
49
+ def save_vectorstore_to_disk(
50
+ onboarding_id: str,
51
+ doc_type: str,
52
+ collection_name: str,
53
+ qdrant_url: Optional[str],
54
+ qdrant_api_key: Optional[str]
55
+ ) -> str:
56
  """
57
+ Previously this created a small local marker file with Qdrant connection details.
58
+ In the diskless version we simply return a logical vectorstore path (URI-style).
59
+ Persisting of metadata is done via `upsert_vectorstore_metadata`.
60
  """
61
+ vs_path = get_vectorstore_path(onboarding_id, doc_type)
62
+ return vs_path
63
+
 
 
64
 
 
65
  def upsert_vectorstore_metadata(
66
  onboarding_id: str,
67
  doc_type: str,
68
  vectorstore_path: str,
69
+ chat_id: str,
70
+ collection_name: Optional[str] = None,
71
+ qdrant_url: Optional[str] = None,
72
+ qdrant_api_key: Optional[str] = None
73
  ) -> None:
74
+ """
75
+ Store metadata in MongoDB. Saves useful fields to allow build_rag_chain to
76
+ reconstruct a working Qdrant client later.
77
+ """
78
+ update = {
79
+ "onboarding_id": onboarding_id,
80
+ "doc_type": doc_type,
81
+ "vectorstore_path": vectorstore_path,
82
+ "chat_id": chat_id,
83
+ "updated_at": datetime.utcnow(),
84
+ }
85
+ if collection_name:
86
+ update["collection_name"] = collection_name
87
+ if qdrant_url:
88
+ update["qdrant_url"] = qdrant_url
89
+ if qdrant_api_key:
90
+ update["qdrant_api_key"] = qdrant_api_key
91
+
92
+ # Upsert the document
93
  vectorstore_meta_coll.update_one(
94
  {"onboarding_id": onboarding_id, "doc_type": doc_type},
95
+ {"$set": update, "$setOnInsert": {"created_at": datetime.utcnow()}},
96
  upsert=True
97
  )
98
+ logger.debug("Upserted vectorstore metadata for %s/%s into Mongo", onboarding_id, doc_type)
99
 
100
 
101
  def get_vectorstore_metadata(
102
  onboarding_id: str,
103
  doc_type: str
104
  ) -> Optional[Dict[str, Any]]:
105
+ """
106
+ Read vectorstore metadata from MongoDB (no local files).
107
+ """
108
+ meta = vectorstore_meta_coll.find_one({"onboarding_id": onboarding_id, "doc_type": doc_type})
109
+ if meta:
110
+ # convert ObjectId or other non-serializable fields if necessary
111
+ return meta
112
+ return None
113
+
114
+
115
+ # ──────────────────────────────────────────────────────────────────────────────
116
+ # Qdrant Retriever (pure Qdrant, Pydantic v2-compatible)
117
+ # ──────────────────────────────────────────────────────────────────────────────
118
+
119
+ class QdrantTextRetriever(BaseRetriever):
120
+ """
121
+ Minimal retriever that queries Qdrant directly and returns LangChain Documents.
122
+ Assumes payload stores the raw chunk under key 'text'.
123
+ """
124
+
125
+ client: QdrantClient
126
+ collection_name: str
127
+ k: int = 5
128
+ model_config = ConfigDict(arbitrary_types_allowed=True)
129
+
130
+ def _get_relevant_documents(self, query: str, *, run_manager=None) -> List[Document]:
131
+ # Embed the query. Try multiple attribute names safely.
132
+ query_vec = None
133
+ for attr in ("embed_query", "embed_documents", "embed_texts", "embed"):
134
+ fn = getattr(embeddings, attr, None)
135
+ if callable(fn):
136
+ try:
137
+ if attr == "embed_query":
138
+ query_vec = fn(query)
139
+ else:
140
+ q_res = fn([query])
141
+ if isinstance(q_res, list) and q_res:
142
+ query_vec = q_res[0]
143
+ else:
144
+ query_vec = q_res
145
+ break
146
+ except Exception:
147
+ continue
148
+ if query_vec is None:
149
+ raise RuntimeError("No usable embedding function available on embeddings object.")
150
+
151
+ # If embedding helpers return dicts
152
+ if isinstance(query_vec, dict) and "embedding" in query_vec:
153
+ query_vec = query_vec["embedding"]
154
 
155
+ # Search Qdrant
156
+ results = self.client.search(
157
+ collection_name=self.collection_name,
158
+ query_vector=query_vec,
159
+ limit=self.k
160
+ )
161
+
162
+ docs: List[Document] = []
163
+ for r in results:
164
+ payload = r.payload or {}
165
+ text = payload.get("text")
166
+ if not isinstance(text, str):
167
+ logger.warning(
168
+ "Qdrant payload missing 'text' or not a string; skipping. Payload: %s",
169
+ payload
170
+ )
171
+ continue
172
+
173
+ metadata = {k: v for k, v in payload.items() if k != "text"}
174
+ metadata["score"] = r.score
175
+
176
+ docs.append(Document(page_content=text, metadata=metadata))
177
+
178
+ return docs
179
+
180
+ async def _aget_relevant_documents(self, query: str, *, run_manager=None) -> List[Document]:
181
+ # For simplicity, use sync path
182
+ return self._get_relevant_documents(query, run_manager=run_manager)
183
+
184
+
185
+ # ──────────────────────────────────────────────────────────────────────────────
186
+ # Build RAG chain (pure Qdrant), using DB metadata (no local files)
187
+ # ──────────────────────────────────────────────────────────────────────────────
188
 
189
  def build_rag_chain(
190
  onboarding_id: str,
 
192
  chat_id: str,
193
  prompt_type: str
194
  ) -> ConversationalRetrievalChain:
195
+ """
196
+ Builds a ConversationalRetrievalChain using pure Qdrant as backend.
197
+ Loads connection details from the MongoDB metadata collection instead of a file.
198
+ If metadata is missing, tries to detect an existing Qdrant collection named
199
+ 'vs_{onboarding_id}_{doc_type}' and auto-registers it in Mongo.
200
+ """
201
+ meta = get_vectorstore_metadata(onboarding_id, doc_type)
202
+
203
+ # If metadata missing — attempt a Qdrant-side fallback detection
204
+ if not meta:
205
+ logger.warning("Vectorstore metadata not found for %s/%s in Mongo; attempting Qdrant fallback detection", onboarding_id, doc_type)
206
+
207
+ # Build a Qdrant client from global settings to detect existing collection
208
+ qdrant_url = getattr(settings, "qdrant_url", None)
209
+ qdrant_api_key = getattr(settings, "qdrant_api_key", None)
210
+ client_kwargs = {}
211
+ if qdrant_url:
212
+ client_kwargs["url"] = qdrant_url
213
+ if qdrant_api_key:
214
+ client_kwargs["api_key"] = qdrant_api_key
215
+
216
+ qdrant_timeout = getattr(settings, "qdrant_timeout", 60)
217
+ prefer_grpc = getattr(settings, "qdrant_prefer_grpc", False)
218
+
219
+ try:
220
+ if client_kwargs:
221
+ qdrant_client = QdrantClient(**client_kwargs, timeout=qdrant_timeout, prefer_grpc=prefer_grpc)
222
+ else:
223
+ qdrant_client = QdrantClient(timeout=qdrant_timeout, prefer_grpc=prefer_grpc)
224
+ except Exception as e:
225
+ logger.exception("Failed to create Qdrant client during fallback detection: %s", e)
226
+ raise HTTPException(status_code=500, detail="Vectorstore metadata not found and failed to connect to Qdrant for fallback detection.")
227
+
228
+ guessed_collection = f"vs_{onboarding_id}_{doc_type}"
229
+ try:
230
+ # get_collection raises if not present; get_collections returns list
231
+ info = None
232
+ try:
233
+ info = qdrant_client.get_collection(collection_name=guessed_collection)
234
+ except Exception:
235
+ # try listing collections (less strict)
236
+ collections_info = qdrant_client.get_collections()
237
+ # get_collections returns a dict-like structure; search names
238
+ found = False
239
+ for c in collections_info.get("collections", []) if isinstance(collections_info, dict) else collections_info:
240
+ name = c.get("name") if isinstance(c, dict) else getattr(c, "name", None)
241
+ if name == guessed_collection:
242
+ found = True
243
+ break
244
+ if not found:
245
+ info = None
246
+ else:
247
+ info = {"name": guessed_collection}
248
+
249
+ if info:
250
+ logger.info("Detected existing Qdrant collection '%s' via fallback; auto-registering metadata in Mongo", guessed_collection)
251
+ # auto-register minimal metadata so chat can proceed
252
+ vs_path = get_vectorstore_path(onboarding_id, doc_type)
253
+ # we don't have a chat_id to store here; store empty string and let setup create chat sessions later
254
+ upsert_vectorstore_metadata(onboarding_id, doc_type, vs_path, chat_id="", collection_name=guessed_collection, qdrant_url=qdrant_url, qdrant_api_key=qdrant_api_key)
255
+ meta = get_vectorstore_metadata(onboarding_id, doc_type)
256
+ else:
257
+ logger.info("Qdrant fallback detection found no collection named '%s'", guessed_collection)
258
+ except Exception as e:
259
+ logger.exception("Error while checking Qdrant collections for fallback detection: %s", e)
260
+ # continue; meta still None and we'll raise below
261
+
262
+ if not meta:
263
+ # Final: helpful error message with actionable next steps
264
+ raise HTTPException(
265
+ status_code=400,
266
+ detail=(
267
+ "Vectorstore metadata not found; run initialization first. "
268
+ "Call POST /rag/initialization/{onboarding_id}/{doc_type} with documents to ingest. "
269
+ "If you already initialized, check server logs for ingestion errors and verify Mongo collection "
270
+ "'vectorstore_meta_coll' contains the record for this onboarding/doc_type."
271
+ )
272
+ )
273
+
274
+ collection_name = meta.get("collection_name")
275
+ if not collection_name:
276
+ raise HTTPException(status_code=500, detail="Qdrant collection name missing in metadata.")
277
+
278
+ # Prefer values from marker; fall back to app settings if needed
279
+ qdrant_url = meta.get("qdrant_url") or getattr(settings, "qdrant_url", None)
280
+ qdrant_api_key = meta.get("qdrant_api_key") or getattr(settings, "qdrant_api_key", None)
281
+
282
+ client_kwargs = {}
283
+ if qdrant_url:
284
+ client_kwargs["url"] = qdrant_url
285
+ if qdrant_api_key:
286
+ client_kwargs["api_key"] = qdrant_api_key
287
+
288
+ qdrant_timeout = getattr(settings, "qdrant_timeout", 60)
289
+ prefer_grpc = getattr(settings, "qdrant_prefer_grpc", False)
290
+
291
+ try:
292
+ if client_kwargs:
293
+ qdrant_client = QdrantClient(**client_kwargs, timeout=qdrant_timeout, prefer_grpc=prefer_grpc)
294
+ else:
295
+ qdrant_client = QdrantClient(timeout=qdrant_timeout, prefer_grpc=prefer_grpc)
296
+ except Exception as e:
297
+ logger.exception("Failed to construct Qdrant client for retrieval: %s", e)
298
+ raise HTTPException(status_code=500, detail=f"Failed to connect to Qdrant: {e}")
299
+
300
+ retriever = QdrantTextRetriever(client=qdrant_client, collection_name=collection_name, k=5)
301
 
302
  # History & memory
303
  chat_history = MongoDBChatMessageHistory(
 
308
  )
309
  memory = ConversationBufferMemory(
310
  memory_key="chat_history",
311
+ chat_memory=chat_history,
312
+ return_messages=True,
313
  )
314
 
315
  llm = get_llm()
316
+
317
  # Choose prompt
318
  if prompt_type == "page_speed":
319
  user_prompt = page_speed_prompt
app/seo/prompts.py CHANGED
@@ -19,6 +19,10 @@ Return *only* a JSON object that has a single top-level key, `priority_suggestio
19
 
20
  Each list item must be a **plain-English sentence**, prefixed with its SEO category tag (e.g. `[On-Page]` or `[Schema]`), and suffixed with `(Effort Level: high|medium|low)`.
21
 
 
 
 
 
22
  {format_instructions}
23
 
24
  Performance Report:
 
19
 
20
  Each list item must be a **plain-English sentence**, prefixed with its SEO category tag (e.g. `[On-Page]` or `[Schema]`), and suffixed with `(Effort Level: high|medium|low)`.
21
 
22
+ Important:
23
+ - Respond with *only* a valid JSON object.
24
+ - Do NOT include any commentary or explanation outside the JSON.
25
+
26
  {format_instructions}
27
 
28
  Performance Report:
app/uiux/prompts.py CHANGED
@@ -21,6 +21,10 @@ Requirements:
21
  6. Ensure the output is strictly JSON—no additional text, comments, or keys.
22
  7. Validate JSON syntax: keys and strings must be enclosed in double quotes.
23
 
 
 
 
 
24
  {format_instructions}
25
 
26
  Input Report Data:
 
21
  6. Ensure the output is strictly JSON—no additional text, comments, or keys.
22
  7. Validate JSON syntax: keys and strings must be enclosed in double quotes.
23
 
24
+ Important:
25
+ - Respond with *only* a valid JSON object.
26
+ - Do NOT include any commentary or explanation outside the JSON.
27
+
28
  {format_instructions}
29
 
30
  Input Report Data:
requirements.txt CHANGED
@@ -7,7 +7,7 @@ pydantic
7
  pydantic_settings
8
  langchain_groq
9
  langchain_community
10
- faiss-cpu
11
  pymongo
12
  langchain-mongodb
13
  langchain_google_genai
 
 
7
  pydantic_settings
8
  langchain_groq
9
  langchain_community
 
10
  pymongo
11
  langchain-mongodb
12
  langchain_google_genai
13
+ qdrant-client