Peterase commited on
Commit
c880083
·
1 Parent(s): 3f02335

feat: folder management, document move/drag-drop, per-document chat scoping

Browse files
app/api/chat.py CHANGED
@@ -7,7 +7,7 @@ from app.auth import get_current_user
7
  from app.models import User
8
  from app.application.chat_service import ChatService
9
  from app.dependencies import get_chat_service
10
- from typing import Optional
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
@@ -17,6 +17,9 @@ router = APIRouter(prefix="/chat", tags=["chat"])
17
  class ChatRequest(BaseModel):
18
  message: str
19
  chat_id: Optional[str] = None
 
 
 
20
 
21
 
22
  @router.post("/")
@@ -25,19 +28,18 @@ async def send_message(
25
  chat_service: ChatService = Depends(get_chat_service),
26
  current_user: User = Depends(get_current_user)
27
  ):
28
- """Send a chat message."""
29
  try:
30
  result = await chat_service.send_message(
31
  message=request.message,
32
  user=current_user,
33
- chat_id=request.chat_id
 
 
34
  )
35
  return result
36
  except ValueError as e:
37
- raise HTTPException(
38
- status_code=status.HTTP_404_NOT_FOUND,
39
- detail=str(e)
40
- )
41
  except Exception as e:
42
  logger.error(f"Chat error: {e}")
43
  raise HTTPException(
@@ -65,10 +67,7 @@ async def get_chat_messages(
65
  try:
66
  return await chat_service.get_chat_messages(chat_id, current_user)
67
  except ValueError as e:
68
- raise HTTPException(
69
- status_code=status.HTTP_404_NOT_FOUND,
70
- detail=str(e)
71
- )
72
 
73
 
74
  @router.delete("/{chat_id}")
@@ -82,7 +81,4 @@ async def delete_chat(
82
  await chat_service.delete_chat(chat_id, current_user)
83
  return {"message": "Chat deleted successfully"}
84
  except ValueError as e:
85
- raise HTTPException(
86
- status_code=status.HTTP_404_NOT_FOUND,
87
- detail=str(e)
88
- )
 
7
  from app.models import User
8
  from app.application.chat_service import ChatService
9
  from app.dependencies import get_chat_service
10
+ from typing import Optional, List
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
 
17
  class ChatRequest(BaseModel):
18
  message: str
19
  chat_id: Optional[str] = None
20
+ # Scope the search to specific documents or a folder
21
+ document_ids: Optional[List[str]] = None # Ask about specific files
22
+ folder_id: Optional[str] = None # Ask about all files in a folder
23
 
24
 
25
  @router.post("/")
 
28
  chat_service: ChatService = Depends(get_chat_service),
29
  current_user: User = Depends(get_current_user)
30
  ):
31
+ """Send a chat message, optionally scoped to specific documents or a folder."""
32
  try:
33
  result = await chat_service.send_message(
34
  message=request.message,
35
  user=current_user,
36
+ chat_id=request.chat_id,
37
+ document_ids=request.document_ids,
38
+ folder_id=request.folder_id
39
  )
40
  return result
41
  except ValueError as e:
42
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
 
 
 
43
  except Exception as e:
44
  logger.error(f"Chat error: {e}")
45
  raise HTTPException(
 
67
  try:
68
  return await chat_service.get_chat_messages(chat_id, current_user)
69
  except ValueError as e:
70
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
 
 
 
71
 
72
 
73
  @router.delete("/{chat_id}")
 
81
  await chat_service.delete_chat(chat_id, current_user)
82
  return {"message": "Chat deleted successfully"}
83
  except ValueError as e:
84
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
 
 
 
app/api/documents.py CHANGED
@@ -1,13 +1,14 @@
1
  """Documents API endpoints."""
2
  from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException, status
 
3
  from sqlalchemy.orm import Session
4
  from app.database import get_db
5
  from app.auth import get_current_user
6
- from app.models import User
7
  from app.application.document_service import DocumentService
8
  from app.dependencies import get_document_service
9
  from app.config import get_settings
10
- from typing import Optional, List
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
@@ -15,6 +16,10 @@ router = APIRouter(prefix="/documents", tags=["documents"])
15
  settings = get_settings()
16
 
17
 
 
 
 
 
18
  @router.post("/upload")
19
  async def upload_document(
20
  file: UploadFile = File(...),
@@ -91,6 +96,42 @@ async def list_documents(
91
  ]
92
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  @router.delete("/{document_id}")
95
  async def delete_document(
96
  document_id: str,
 
1
  """Documents API endpoints."""
2
  from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException, status
3
+ from pydantic import BaseModel
4
  from sqlalchemy.orm import Session
5
  from app.database import get_db
6
  from app.auth import get_current_user
7
+ from app.models import User, Document
8
  from app.application.document_service import DocumentService
9
  from app.dependencies import get_document_service
10
  from app.config import get_settings
11
+ from typing import Optional
12
  import logging
13
 
14
  logger = logging.getLogger(__name__)
 
16
  settings = get_settings()
17
 
18
 
19
+ class MoveDocumentRequest(BaseModel):
20
+ folder_id: Optional[str] = None # None = move to root
21
+
22
+
23
  @router.post("/upload")
24
  async def upload_document(
25
  file: UploadFile = File(...),
 
96
  ]
97
 
98
 
99
+ @router.patch("/{document_id}/move")
100
+ async def move_document(
101
+ document_id: str,
102
+ request: MoveDocumentRequest,
103
+ db: Session = Depends(get_db),
104
+ current_user: User = Depends(get_current_user)
105
+ ):
106
+ """Move a document to a folder or to root (drag & drop support)."""
107
+ document = db.query(Document).filter(
108
+ Document.id == document_id,
109
+ Document.org_id == current_user.org_id
110
+ ).first()
111
+ if not document:
112
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
113
+
114
+ # Validate folder belongs to same org
115
+ if request.folder_id:
116
+ from app.models import Folder
117
+ folder = db.query(Folder).filter(
118
+ Folder.id == request.folder_id,
119
+ Folder.org_id == current_user.org_id
120
+ ).first()
121
+ if not folder:
122
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
123
+
124
+ document.folder_id = request.folder_id
125
+ db.commit()
126
+
127
+ return {
128
+ "id": document.id,
129
+ "name": document.name,
130
+ "folder_id": document.folder_id,
131
+ "message": "Document moved successfully"
132
+ }
133
+
134
+
135
  @router.delete("/{document_id}")
136
  async def delete_document(
137
  document_id: str,
app/api/folders.py CHANGED
@@ -1,35 +1,54 @@
1
  """Folders API endpoints."""
2
  from fastapi import APIRouter, Depends, HTTPException, status
 
3
  from sqlalchemy.orm import Session
4
  from app.database import get_db
5
  from app.auth import get_current_user
6
- from app.models import User, Folder
 
7
  import logging
8
 
9
  logger = logging.getLogger(__name__)
10
  router = APIRouter(prefix="/folders", tags=["folders"])
11
 
12
 
13
- @router.post("/")
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  async def create_folder(
15
- name: str,
16
  db: Session = Depends(get_db),
17
  current_user: User = Depends(get_current_user)
18
  ):
19
- """Create a folder."""
20
- folder = Folder(
21
- name=name,
22
- org_id=current_user.org_id
23
- )
 
 
 
 
 
 
 
 
24
  db.add(folder)
25
  db.commit()
26
  db.refresh(folder)
27
-
28
- return {
29
- "id": folder.id,
30
- "name": folder.name,
31
- "created_at": folder.created_at.isoformat()
32
- }
33
 
34
 
35
  @router.get("/")
@@ -37,40 +56,164 @@ async def list_folders(
37
  db: Session = Depends(get_db),
38
  current_user: User = Depends(get_current_user)
39
  ):
40
- """List all folders."""
41
  folders = db.query(Folder).filter(
42
  Folder.org_id == current_user.org_id
43
  ).order_by(Folder.created_at.desc()).all()
44
-
45
- return [
46
- {
47
- "id": folder.id,
48
- "name": folder.name,
49
- "created_at": folder.created_at.isoformat()
50
- }
51
- for folder in folders
52
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
55
  @router.delete("/{folder_id}")
56
  async def delete_folder(
57
  folder_id: str,
 
58
  db: Session = Depends(get_db),
59
  current_user: User = Depends(get_current_user)
60
  ):
61
- """Delete a folder."""
62
- folder = db.query(Folder).filter(
63
- Folder.id == folder_id,
64
- Folder.org_id == current_user.org_id
65
- ).first()
66
-
67
- if not folder:
68
  raise HTTPException(
69
- status_code=status.HTTP_404_NOT_FOUND,
70
- detail="Folder not found"
71
  )
72
-
 
 
 
 
73
  db.delete(folder)
74
  db.commit()
75
-
76
  return {"message": "Folder deleted successfully"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Folders API endpoints."""
2
  from fastapi import APIRouter, Depends, HTTPException, status
3
+ from pydantic import BaseModel
4
  from sqlalchemy.orm import Session
5
  from app.database import get_db
6
  from app.auth import get_current_user
7
+ from app.models import User, Folder, Document
8
+ from typing import Optional
9
  import logging
10
 
11
  logger = logging.getLogger(__name__)
12
  router = APIRouter(prefix="/folders", tags=["folders"])
13
 
14
 
15
+ class CreateFolderRequest(BaseModel):
16
+ name: str
17
+
18
+
19
+ class RenameFolderRequest(BaseModel):
20
+ name: str
21
+
22
+
23
+ class MoveDocumentRequest(BaseModel):
24
+ document_id: str
25
+ folder_id: Optional[str] = None # None = move to root
26
+
27
+
28
+ @router.post("/", status_code=status.HTTP_201_CREATED)
29
  async def create_folder(
30
+ request: CreateFolderRequest,
31
  db: Session = Depends(get_db),
32
  current_user: User = Depends(get_current_user)
33
  ):
34
+ """Create a new folder."""
35
+ # Check for duplicate name in same org
36
+ existing = db.query(Folder).filter(
37
+ Folder.org_id == current_user.org_id,
38
+ Folder.name == request.name
39
+ ).first()
40
+ if existing:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_400_BAD_REQUEST,
43
+ detail=f"Folder '{request.name}' already exists"
44
+ )
45
+
46
+ folder = Folder(name=request.name, org_id=current_user.org_id)
47
  db.add(folder)
48
  db.commit()
49
  db.refresh(folder)
50
+
51
+ return _folder_response(folder, db)
 
 
 
 
52
 
53
 
54
  @router.get("/")
 
56
  db: Session = Depends(get_db),
57
  current_user: User = Depends(get_current_user)
58
  ):
59
+ """List all folders with document counts."""
60
  folders = db.query(Folder).filter(
61
  Folder.org_id == current_user.org_id
62
  ).order_by(Folder.created_at.desc()).all()
63
+
64
+ return [_folder_response(f, db) for f in folders]
65
+
66
+
67
+ @router.get("/{folder_id}")
68
+ async def get_folder(
69
+ folder_id: str,
70
+ db: Session = Depends(get_db),
71
+ current_user: User = Depends(get_current_user)
72
+ ):
73
+ """Get a folder with its documents."""
74
+ folder = _get_folder_or_404(folder_id, current_user.org_id, db)
75
+
76
+ documents = db.query(Document).filter(
77
+ Document.folder_id == folder_id,
78
+ Document.org_id == current_user.org_id
79
+ ).order_by(Document.created_at.desc()).all()
80
+
81
+ return {
82
+ **_folder_response(folder, db),
83
+ "documents": [_doc_response(d) for d in documents]
84
+ }
85
+
86
+
87
+ @router.patch("/{folder_id}")
88
+ async def rename_folder(
89
+ folder_id: str,
90
+ request: RenameFolderRequest,
91
+ db: Session = Depends(get_db),
92
+ current_user: User = Depends(get_current_user)
93
+ ):
94
+ """Rename a folder."""
95
+ folder = _get_folder_or_404(folder_id, current_user.org_id, db)
96
+
97
+ # Check duplicate name
98
+ existing = db.query(Folder).filter(
99
+ Folder.org_id == current_user.org_id,
100
+ Folder.name == request.name,
101
+ Folder.id != folder_id
102
+ ).first()
103
+ if existing:
104
+ raise HTTPException(
105
+ status_code=status.HTTP_400_BAD_REQUEST,
106
+ detail=f"Folder '{request.name}' already exists"
107
+ )
108
+
109
+ folder.name = request.name
110
+ db.commit()
111
+ db.refresh(folder)
112
+
113
+ return _folder_response(folder, db)
114
+
115
+
116
+ @router.post("/{folder_id}/move-document")
117
+ async def move_document_to_folder(
118
+ folder_id: str,
119
+ request: MoveDocumentRequest,
120
+ db: Session = Depends(get_db),
121
+ current_user: User = Depends(get_current_user)
122
+ ):
123
+ """Move a document into this folder (supports drag & drop)."""
124
+ _get_folder_or_404(folder_id, current_user.org_id, db)
125
+
126
+ document = db.query(Document).filter(
127
+ Document.id == request.document_id,
128
+ Document.org_id == current_user.org_id
129
+ ).first()
130
+ if not document:
131
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
132
+
133
+ document.folder_id = folder_id
134
+ db.commit()
135
+
136
+ return {"message": "Document moved successfully", "document_id": request.document_id, "folder_id": folder_id}
137
+
138
+
139
+ @router.delete("/{folder_id}/move-document")
140
+ async def remove_document_from_folder(
141
+ folder_id: str,
142
+ request: MoveDocumentRequest,
143
+ db: Session = Depends(get_db),
144
+ current_user: User = Depends(get_current_user)
145
+ ):
146
+ """Remove a document from a folder (move to root)."""
147
+ document = db.query(Document).filter(
148
+ Document.id == request.document_id,
149
+ Document.org_id == current_user.org_id,
150
+ Document.folder_id == folder_id
151
+ ).first()
152
+ if not document:
153
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found in this folder")
154
+
155
+ document.folder_id = None
156
+ db.commit()
157
+
158
+ return {"message": "Document removed from folder", "document_id": request.document_id}
159
 
160
 
161
  @router.delete("/{folder_id}")
162
  async def delete_folder(
163
  folder_id: str,
164
+ force: bool = False,
165
  db: Session = Depends(get_db),
166
  current_user: User = Depends(get_current_user)
167
  ):
168
+ """Delete a folder. Use ?force=true to also move documents to root."""
169
+ folder = _get_folder_or_404(folder_id, current_user.org_id, db)
170
+
171
+ doc_count = db.query(Document).filter(Document.folder_id == folder_id).count()
172
+
173
+ if doc_count > 0 and not force:
 
174
  raise HTTPException(
175
+ status_code=status.HTTP_400_BAD_REQUEST,
176
+ detail=f"Folder has {doc_count} document(s). Use ?force=true to delete and move documents to root."
177
  )
178
+
179
+ if force:
180
+ # Move all documents to root
181
+ db.query(Document).filter(Document.folder_id == folder_id).update({"folder_id": None})
182
+
183
  db.delete(folder)
184
  db.commit()
185
+
186
  return {"message": "Folder deleted successfully"}
187
+
188
+
189
+ # ── helpers ──────────────────────────────────────────────────────────────────
190
+
191
+ def _get_folder_or_404(folder_id: str, org_id: str, db: Session) -> Folder:
192
+ folder = db.query(Folder).filter(
193
+ Folder.id == folder_id,
194
+ Folder.org_id == org_id
195
+ ).first()
196
+ if not folder:
197
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
198
+ return folder
199
+
200
+
201
+ def _folder_response(folder: Folder, db: Session) -> dict:
202
+ doc_count = db.query(Document).filter(Document.folder_id == folder.id).count()
203
+ return {
204
+ "id": folder.id,
205
+ "name": folder.name,
206
+ "document_count": doc_count,
207
+ "created_at": folder.created_at.isoformat()
208
+ }
209
+
210
+
211
+ def _doc_response(doc: Document) -> dict:
212
+ return {
213
+ "id": doc.id,
214
+ "name": doc.name,
215
+ "size": doc.size,
216
+ "chunks": doc.chunks,
217
+ "folder_id": doc.folder_id,
218
+ "created_at": doc.created_at.isoformat()
219
+ }
app/application/chat_service.py CHANGED
@@ -32,11 +32,13 @@ class ChatService:
32
  self,
33
  message: str,
34
  user: User,
35
- chat_id: Optional[str] = None
 
 
36
  ) -> Dict[str, Any]:
37
  """Send message and get AI response."""
38
  logger.info(f"Processing message for user {user.email}")
39
-
40
  # Get or create chat
41
  if chat_id:
42
  chat = self.db.query(Chat).filter(
@@ -46,37 +48,41 @@ class ChatService:
46
  if not chat:
47
  raise ValueError("Chat not found")
48
  else:
49
- # Create new chat with first message as title
50
  title = message[:50] + "..." if len(message) > 50 else message
51
- chat = Chat(
52
- title=title,
53
- user_id=user.id,
54
- org_id=user.org_id
55
- )
56
  self.db.add(chat)
57
  self.db.commit()
58
  self.db.refresh(chat)
59
-
60
  # Store user message
61
- user_message = Message(
62
- chat_id=chat.id,
63
- role="user",
64
- content=message
65
- )
66
  self.db.add(user_message)
67
  self.db.commit()
68
-
 
 
 
 
 
 
 
 
 
 
 
 
69
  # 1. Embed query
70
  query_embedding = await self.embedder.embed_text(message)
71
-
72
- # 2. Search vector DB
73
  search_results = await self.vector_db.search(
74
  query_embedding=query_embedding,
75
  org_id=user.org_id,
76
  top_k=settings.TOP_K_RETRIEVAL,
77
- collection_name=settings.QDRANT_COLLECTION
 
78
  )
79
-
80
  # 3. Build context and sources
81
  context_chunks = [result.text for result in search_results]
82
  sources = [
@@ -88,38 +94,45 @@ class ChatService:
88
  }
89
  for result in search_results
90
  ]
91
-
92
  # 4. Get chat history
93
  chat_history = self._get_chat_history(chat.id)
94
-
95
  # 5. Generate answer
96
  if not context_chunks:
97
- answer = "I cannot find any relevant information in your documents. Please upload documents related to your question."
 
 
 
98
  else:
 
 
 
 
 
 
99
  llm_response = await self.llm.generate_answer(
100
- question=message,
101
  context_chunks=context_chunks,
102
  chat_history=chat_history
103
  )
104
  answer = llm_response.answer
105
-
106
  # 6. Store assistant message
 
 
 
 
 
 
107
  assistant_message = Message(
108
- chat_id=chat.id,
109
  role="assistant",
110
  content=answer,
111
  sources=json.dumps(sources)
112
  )
113
  self.db.add(assistant_message)
114
  self.db.commit()
115
-
116
- logger.info(f"Generated response for chat {chat.id}")
117
-
118
- return {
119
- "answer": answer,
120
- "sources": sources,
121
- "chat_id": chat.id
122
- }
123
 
124
  def _get_chat_history(self, chat_id: str) -> List[LLMMessage]:
125
  """Get recent chat history."""
 
32
  self,
33
  message: str,
34
  user: User,
35
+ chat_id: Optional[str] = None,
36
+ document_ids: Optional[List[str]] = None,
37
+ folder_id: Optional[str] = None
38
  ) -> Dict[str, Any]:
39
  """Send message and get AI response."""
40
  logger.info(f"Processing message for user {user.email}")
41
+
42
  # Get or create chat
43
  if chat_id:
44
  chat = self.db.query(Chat).filter(
 
48
  if not chat:
49
  raise ValueError("Chat not found")
50
  else:
 
51
  title = message[:50] + "..." if len(message) > 50 else message
52
+ chat = Chat(title=title, user_id=user.id, org_id=user.org_id)
 
 
 
 
53
  self.db.add(chat)
54
  self.db.commit()
55
  self.db.refresh(chat)
56
+
57
  # Store user message
58
+ user_message = Message(chat_id=chat.id, role="user", content=message)
 
 
 
 
59
  self.db.add(user_message)
60
  self.db.commit()
61
+
62
+ # Resolve document_ids from folder if folder_id provided
63
+ if folder_id and not document_ids:
64
+ docs = self.db.query(Document).filter(
65
+ Document.folder_id == folder_id,
66
+ Document.org_id == user.org_id
67
+ ).all()
68
+ document_ids = [d.id for d in docs]
69
+ if not document_ids:
70
+ answer = f"The selected folder has no documents yet. Please upload files to it first."
71
+ self._save_assistant_message(chat.id, answer, [])
72
+ return {"answer": answer, "sources": [], "chat_id": chat.id}
73
+
74
  # 1. Embed query
75
  query_embedding = await self.embedder.embed_text(message)
76
+
77
+ # 2. Search vector DB (scoped or global)
78
  search_results = await self.vector_db.search(
79
  query_embedding=query_embedding,
80
  org_id=user.org_id,
81
  top_k=settings.TOP_K_RETRIEVAL,
82
+ collection_name=settings.QDRANT_COLLECTION,
83
+ document_ids=document_ids # None = search all docs
84
  )
85
+
86
  # 3. Build context and sources
87
  context_chunks = [result.text for result in search_results]
88
  sources = [
 
94
  }
95
  for result in search_results
96
  ]
97
+
98
  # 4. Get chat history
99
  chat_history = self._get_chat_history(chat.id)
100
+
101
  # 5. Generate answer
102
  if not context_chunks:
103
+ if document_ids:
104
+ answer = "I couldn't find relevant information in the selected document(s). Try asking a different question or select different files."
105
+ else:
106
+ answer = "I cannot find any relevant information in your documents. Please upload documents related to your question."
107
  else:
108
+ # Add scope context to the question if scoped
109
+ scoped_question = message
110
+ if document_ids:
111
+ doc_names = list({s["document_name"] for s in sources})
112
+ scoped_question = f"[Searching in: {', '.join(doc_names)}]\n\n{message}"
113
+
114
  llm_response = await self.llm.generate_answer(
115
+ question=scoped_question,
116
  context_chunks=context_chunks,
117
  chat_history=chat_history
118
  )
119
  answer = llm_response.answer
120
+
121
  # 6. Store assistant message
122
+ self._save_assistant_message(chat.id, answer, sources)
123
+
124
+ logger.info(f"Generated response for chat {chat.id}")
125
+ return {"answer": answer, "sources": sources, "chat_id": chat.id}
126
+
127
+ def _save_assistant_message(self, chat_id: str, answer: str, sources: list):
128
  assistant_message = Message(
129
+ chat_id=chat_id,
130
  role="assistant",
131
  content=answer,
132
  sources=json.dumps(sources)
133
  )
134
  self.db.add(assistant_message)
135
  self.db.commit()
 
 
 
 
 
 
 
 
136
 
137
  def _get_chat_history(self, chat_id: str) -> List[LLMMessage]:
138
  """Get recent chat history."""
app/ports/vector_db.py CHANGED
@@ -44,7 +44,8 @@ class VectorDBPort(ABC):
44
  query_embedding: List[float],
45
  org_id: str,
46
  top_k: int,
47
- collection_name: str
 
48
  ) -> List[VectorSearchResult]:
49
  """Search for similar vectors."""
50
  pass
 
44
  query_embedding: List[float],
45
  org_id: str,
46
  top_k: int,
47
+ collection_name: str,
48
+ document_ids: List[str] = None # None = search all docs
49
  ) -> List[VectorSearchResult]:
50
  """Search for similar vectors."""
51
  pass
app/services/pinecone_adapter.py CHANGED
@@ -106,18 +106,29 @@ class PineconeAdapter(VectorDBPort):
106
  query_embedding: List[float],
107
  org_id: str,
108
  top_k: int,
109
- collection_name: str
 
110
  ) -> List[VectorSearchResult]:
111
  """Search for similar vectors in Pinecone."""
112
  if self.index is None:
113
  raise RuntimeError("Pinecone client not available")
114
-
115
  try:
116
- # Query Pinecone with org_id filter
 
 
 
 
 
 
 
 
 
 
117
  results = self.index.query(
118
  vector=query_embedding,
119
  top_k=top_k,
120
- filter={"org_id": {"$eq": org_id}},
121
  include_metadata=True
122
  )
123
 
 
106
  query_embedding: List[float],
107
  org_id: str,
108
  top_k: int,
109
+ collection_name: str,
110
+ document_ids: List[str] = None
111
  ) -> List[VectorSearchResult]:
112
  """Search for similar vectors in Pinecone."""
113
  if self.index is None:
114
  raise RuntimeError("Pinecone client not available")
115
+
116
  try:
117
+ # Build filter
118
+ if document_ids:
119
+ # Scope to specific documents
120
+ pinecone_filter = {
121
+ "org_id": {"$eq": org_id},
122
+ "document_id": {"$in": document_ids}
123
+ }
124
+ else:
125
+ # Search all docs in org
126
+ pinecone_filter = {"org_id": {"$eq": org_id}}
127
+
128
  results = self.index.query(
129
  vector=query_embedding,
130
  top_k=top_k,
131
+ filter=pinecone_filter,
132
  include_metadata=True
133
  )
134