morphik 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
morphik/sync.py CHANGED
@@ -8,9 +8,9 @@ import httpx
8
8
  from pydantic import BaseModel
9
9
 
10
10
  from ._internal import FinalChunkResult, RuleOrDict, _MorphikClientLogic
11
+ from .models import CompletionResponse # Prompt override models
11
12
  from .models import (
12
13
  ChunkSource,
13
- CompletionResponse, # Prompt override models
14
14
  Document,
15
15
  DocumentResult,
16
16
  FolderInfo,
@@ -287,6 +287,7 @@ class Folder:
287
287
  k: int = 4,
288
288
  min_score: float = 0.0,
289
289
  use_colpali: bool = True,
290
+ additional_folders: Optional[List[str]] = None,
290
291
  ) -> List[FinalChunkResult]:
291
292
  """
292
293
  Retrieve relevant chunks within this folder.
@@ -297,17 +298,19 @@ class Folder:
297
298
  k: Number of results (default: 4)
298
299
  min_score: Minimum similarity threshold (default: 0.0)
299
300
  use_colpali: Whether to use ColPali-style embedding model
301
+ additional_folders: Optional list of extra folders to include in the scope
300
302
 
301
303
  Returns:
302
304
  List[FinalChunkResult]: List of relevant chunks
303
305
  """
306
+ effective_folder = self._merge_folders(additional_folders)
304
307
  request = {
305
308
  "query": query,
306
309
  "filters": filters,
307
310
  "k": k,
308
311
  "min_score": min_score,
309
312
  "use_colpali": use_colpali,
310
- "folder_name": self._name, # Add folder name here
313
+ "folder_name": effective_folder,
311
314
  }
312
315
 
313
316
  response = self._client._request("POST", "retrieve/chunks", request)
@@ -320,6 +323,7 @@ class Folder:
320
323
  k: int = 4,
321
324
  min_score: float = 0.0,
322
325
  use_colpali: bool = True,
326
+ additional_folders: Optional[List[str]] = None,
323
327
  ) -> List[DocumentResult]:
324
328
  """
325
329
  Retrieve relevant documents within this folder.
@@ -330,17 +334,19 @@ class Folder:
330
334
  k: Number of results (default: 4)
331
335
  min_score: Minimum similarity threshold (default: 0.0)
332
336
  use_colpali: Whether to use ColPali-style embedding model
337
+ additional_folders: Optional list of extra folders to include in the scope
333
338
 
334
339
  Returns:
335
340
  List[DocumentResult]: List of relevant documents
336
341
  """
342
+ effective_folder = self._merge_folders(additional_folders)
337
343
  request = {
338
344
  "query": query,
339
345
  "filters": filters,
340
346
  "k": k,
341
347
  "min_score": min_score,
342
348
  "use_colpali": use_colpali,
343
- "folder_name": self._name, # Add folder name here
349
+ "folder_name": effective_folder,
344
350
  }
345
351
 
346
352
  response = self._client._request("POST", "retrieve/docs", request)
@@ -359,6 +365,7 @@ class Folder:
359
365
  hop_depth: int = 1,
360
366
  include_paths: bool = False,
361
367
  prompt_overrides: Optional[Union[QueryPromptOverrides, Dict[str, Any]]] = None,
368
+ additional_folders: Optional[List[str]] = None,
362
369
  schema: Optional[Union[Type[BaseModel], Dict[str, Any]]] = None,
363
370
  ) -> CompletionResponse:
364
371
  """
@@ -376,11 +383,13 @@ class Folder:
376
383
  hop_depth: Number of relationship hops to traverse in the graph (1-3)
377
384
  include_paths: Whether to include relationship paths in the response
378
385
  prompt_overrides: Optional customizations for entity extraction, resolution, and query prompts
386
+ additional_folders: Optional list of extra folders to include in the scope
379
387
  schema: Optional schema for structured output
380
388
 
381
389
  Returns:
382
390
  CompletionResponse: Generated completion
383
391
  """
392
+ effective_folder = self._merge_folders(additional_folders)
384
393
  payload = self._client._logic._prepare_query_request(
385
394
  query,
386
395
  filters,
@@ -393,8 +402,8 @@ class Folder:
393
402
  hop_depth,
394
403
  include_paths,
395
404
  prompt_overrides,
396
- self._name,
397
- None,
405
+ effective_folder,
406
+ None, # end_user_id not supported at this level
398
407
  schema,
399
408
  )
400
409
 
@@ -413,7 +422,11 @@ class Folder:
413
422
  return self._client._logic._parse_completion_response(response)
414
423
 
415
424
  def list_documents(
416
- self, skip: int = 0, limit: int = 100, filters: Optional[Dict[str, Any]] = None
425
+ self,
426
+ skip: int = 0,
427
+ limit: int = 100,
428
+ filters: Optional[Dict[str, Any]] = None,
429
+ additional_folders: Optional[List[str]] = None,
417
430
  ) -> List[Document]:
418
431
  """
419
432
  List accessible documents within this folder.
@@ -422,28 +435,34 @@ class Folder:
422
435
  skip: Number of documents to skip
423
436
  limit: Maximum number of documents to return
424
437
  filters: Optional filters
438
+ additional_folders: Optional list of extra folders to include in the scope
425
439
 
426
440
  Returns:
427
441
  List[Document]: List of documents
428
442
  """
429
- params, data = self._client._logic._prepare_list_documents_request(skip, limit, filters, self._name, None)
443
+ effective_folder = self._merge_folders(additional_folders)
444
+ params, data = self._client._logic._prepare_list_documents_request(skip, limit, filters, effective_folder, None)
430
445
  response = self._client._request("POST", "documents", data=data, params=params)
431
446
  docs = self._client._logic._parse_document_list_response(response)
432
447
  for doc in docs:
433
448
  doc._client = self._client
434
449
  return docs
435
450
 
436
- def batch_get_documents(self, document_ids: List[str]) -> List[Document]:
451
+ def batch_get_documents(
452
+ self, document_ids: List[str], additional_folders: Optional[List[str]] = None
453
+ ) -> List[Document]:
437
454
  """
438
455
  Retrieve multiple documents by their IDs in a single batch operation within this folder.
439
456
 
440
457
  Args:
441
458
  document_ids: List of document IDs to retrieve
459
+ additional_folders: Optional list of extra folders to include in the scope
442
460
 
443
461
  Returns:
444
462
  List[Document]: List of document metadata for found documents
445
463
  """
446
- request = {"document_ids": document_ids, "folder_name": self._name}
464
+ merged = self._merge_folders(additional_folders)
465
+ request = {"document_ids": document_ids, "folder_name": merged}
447
466
 
448
467
  response = self._client._request("POST", "batch/documents", data=request)
449
468
  docs = [self._client._logic._parse_document_response(doc) for doc in response]
@@ -451,12 +470,17 @@ class Folder:
451
470
  doc._client = self._client
452
471
  return docs
453
472
 
454
- def batch_get_chunks(self, sources: List[Union[ChunkSource, Dict[str, Any]]]) -> List[FinalChunkResult]:
473
+ def batch_get_chunks(
474
+ self,
475
+ sources: List[Union[ChunkSource, Dict[str, Any]]],
476
+ additional_folders: Optional[List[str]] = None,
477
+ ) -> List[FinalChunkResult]:
455
478
  """
456
479
  Retrieve specific chunks by their document ID and chunk number in a single batch operation within this folder.
457
480
 
458
481
  Args:
459
482
  sources: List of ChunkSource objects or dictionaries with document_id and chunk_number
483
+ additional_folders: Optional list of extra folders to include in the scope
460
484
 
461
485
  Returns:
462
486
  List[FinalChunkResult]: List of chunk results
@@ -469,8 +493,8 @@ class Folder:
469
493
  else:
470
494
  source_dicts.append(source.model_dump())
471
495
 
472
- # Add folder_name to request
473
- request = {"sources": source_dicts, "folder_name": self._name}
496
+ merged = self._merge_folders(additional_folders)
497
+ request = {"sources": source_dicts, "folder_name": merged}
474
498
 
475
499
  response = self._client._request("POST", "batch/chunks", data=request)
476
500
  return self._client._logic._parse_chunk_result_list_response(response)
@@ -507,7 +531,9 @@ class Folder:
507
531
  }
508
532
 
509
533
  response = self._client._request("POST", "graph/create", request)
510
- return self._client._logic._parse_graph_response(response)
534
+ graph = self._logic._parse_graph_response(response)
535
+ graph._client = self
536
+ return graph
511
537
 
512
538
  def update_graph(
513
539
  self,
@@ -540,7 +566,9 @@ class Folder:
540
566
  }
541
567
 
542
568
  response = self._client._request("POST", f"graph/{name}/update", request)
543
- return self._client._logic._parse_graph_response(response)
569
+ graph = self._logic._parse_graph_response(response)
570
+ graph._client = self
571
+ return graph
544
572
 
545
573
  def delete_document_by_filename(self, filename: str) -> Dict[str, str]:
546
574
  """
@@ -559,6 +587,21 @@ class Folder:
559
587
  # Then delete by ID
560
588
  return self._client.delete_document(doc.external_id)
561
589
 
590
+ # Helper --------------------------------------------------------------
591
+ def _merge_folders(self, additional_folders: Optional[List[str]] = None) -> Union[str, List[str]]:
592
+ """Return the effective folder scope.
593
+
594
+ If *additional_folders* is provided it will be combined with the folder's
595
+ own *self._name* and returned as a list (to preserve ordering and allow
596
+ duplicates to be removed server-side). Otherwise just *self._name* is
597
+ returned so we keep backward-compatibility with the original API that
598
+ expected a single string.
599
+ """
600
+ if not additional_folders:
601
+ return self._name
602
+ # Pre-pend the scoped folder to the list provided by the caller.
603
+ return [self._name] + additional_folders
604
+
562
605
 
563
606
  class UserScope:
564
607
  """
@@ -824,6 +867,7 @@ class UserScope:
824
867
  k: int = 4,
825
868
  min_score: float = 0.0,
826
869
  use_colpali: bool = True,
870
+ additional_folders: Optional[List[str]] = None,
827
871
  ) -> List[FinalChunkResult]:
828
872
  """
829
873
  Retrieve relevant chunks as this end user.
@@ -834,10 +878,12 @@ class UserScope:
834
878
  k: Number of results (default: 4)
835
879
  min_score: Minimum similarity threshold (default: 0.0)
836
880
  use_colpali: Whether to use ColPali-style embedding model
881
+ additional_folders: Optional list of extra folders to include in the scope
837
882
 
838
883
  Returns:
839
884
  List[FinalChunkResult]: List of relevant chunks
840
885
  """
886
+ effective_folder = self._merge_folders(additional_folders)
841
887
  request = {
842
888
  "query": query,
843
889
  "filters": filters,
@@ -845,6 +891,7 @@ class UserScope:
845
891
  "min_score": min_score,
846
892
  "use_colpali": use_colpali,
847
893
  "end_user_id": self._end_user_id, # Add end user ID here
894
+ "folder_name": effective_folder, # Add folder name if provided
848
895
  }
849
896
 
850
897
  # Add folder name if scoped to a folder
@@ -861,6 +908,7 @@ class UserScope:
861
908
  k: int = 4,
862
909
  min_score: float = 0.0,
863
910
  use_colpali: bool = True,
911
+ additional_folders: Optional[List[str]] = None,
864
912
  ) -> List[DocumentResult]:
865
913
  """
866
914
  Retrieve relevant documents as this end user.
@@ -871,10 +919,12 @@ class UserScope:
871
919
  k: Number of results (default: 4)
872
920
  min_score: Minimum similarity threshold (default: 0.0)
873
921
  use_colpali: Whether to use ColPali-style embedding model
922
+ additional_folders: Optional list of extra folders to include in the scope
874
923
 
875
924
  Returns:
876
925
  List[DocumentResult]: List of relevant documents
877
926
  """
927
+ effective_folder = self._merge_folders(additional_folders)
878
928
  request = {
879
929
  "query": query,
880
930
  "filters": filters,
@@ -882,6 +932,7 @@ class UserScope:
882
932
  "min_score": min_score,
883
933
  "use_colpali": use_colpali,
884
934
  "end_user_id": self._end_user_id, # Add end user ID here
935
+ "folder_name": effective_folder, # Add folder name if provided
885
936
  }
886
937
 
887
938
  # Add folder name if scoped to a folder
@@ -904,6 +955,7 @@ class UserScope:
904
955
  hop_depth: int = 1,
905
956
  include_paths: bool = False,
906
957
  prompt_overrides: Optional[Union[QueryPromptOverrides, Dict[str, Any]]] = None,
958
+ additional_folders: Optional[List[str]] = None,
907
959
  schema: Optional[Union[Type[BaseModel], Dict[str, Any]]] = None,
908
960
  ) -> CompletionResponse:
909
961
  """
@@ -921,11 +973,13 @@ class UserScope:
921
973
  hop_depth: Number of relationship hops to traverse in the graph (1-3)
922
974
  include_paths: Whether to include relationship paths in the response
923
975
  prompt_overrides: Optional customizations for entity extraction, resolution, and query prompts
976
+ additional_folders: Optional list of extra folders to include in the scope
924
977
  schema: Optional schema for structured output
925
978
 
926
979
  Returns:
927
980
  CompletionResponse: Generated completion
928
981
  """
982
+ effective_folder = self._merge_folders(additional_folders)
929
983
  payload = self._client._logic._prepare_query_request(
930
984
  query,
931
985
  filters,
@@ -938,7 +992,7 @@ class UserScope:
938
992
  hop_depth,
939
993
  include_paths,
940
994
  prompt_overrides,
941
- self._folder_name,
995
+ effective_folder,
942
996
  self._end_user_id,
943
997
  schema,
944
998
  )
@@ -958,7 +1012,11 @@ class UserScope:
958
1012
  return self._client._logic._parse_completion_response(response)
959
1013
 
960
1014
  def list_documents(
961
- self, skip: int = 0, limit: int = 100, filters: Optional[Dict[str, Any]] = None
1015
+ self,
1016
+ skip: int = 0,
1017
+ limit: int = 100,
1018
+ filters: Optional[Dict[str, Any]] = None,
1019
+ additional_folders: Optional[List[str]] = None,
962
1020
  ) -> List[Document]:
963
1021
  """
964
1022
  List accessible documents for this end user.
@@ -967,6 +1025,7 @@ class UserScope:
967
1025
  skip: Number of documents to skip
968
1026
  limit: Maximum number of documents to return
969
1027
  filters: Optional filters
1028
+ additional_folders: Optional list of extra folders to include in the scope
970
1029
 
971
1030
  Returns:
972
1031
  List[Document]: List of documents
@@ -978,6 +1037,11 @@ class UserScope:
978
1037
  if self._folder_name:
979
1038
  params["folder_name"] = self._folder_name
980
1039
 
1040
+ # Merge any additional folders into the request params
1041
+ effective_folder = self._merge_folders(additional_folders)
1042
+ if effective_folder:
1043
+ params["folder_name"] = effective_folder
1044
+
981
1045
  response = self._client._request("POST", "documents", data=filters or {}, params=params)
982
1046
 
983
1047
  docs = [self._client._logic._parse_document_response(doc) for doc in response]
@@ -985,21 +1049,24 @@ class UserScope:
985
1049
  doc._client = self._client
986
1050
  return docs
987
1051
 
988
- def batch_get_documents(self, document_ids: List[str]) -> List[Document]:
1052
+ def batch_get_documents(
1053
+ self, document_ids: List[str], additional_folders: Optional[List[str]] = None
1054
+ ) -> List[Document]:
989
1055
  """
990
1056
  Retrieve multiple documents by their IDs in a single batch operation for this end user.
991
1057
 
992
1058
  Args:
993
1059
  document_ids: List of document IDs to retrieve
1060
+ additional_folders: Optional list of extra folders to include in the scope
994
1061
 
995
1062
  Returns:
996
1063
  List[Document]: List of document metadata for found documents
997
1064
  """
1065
+ merged = self._merge_folders(additional_folders)
998
1066
  request = {"document_ids": document_ids, "end_user_id": self._end_user_id}
999
1067
 
1000
- # Add folder name if scoped to a folder
1001
- if self._folder_name:
1002
- request["folder_name"] = self._folder_name
1068
+ if merged:
1069
+ request["folder_name"] = merged
1003
1070
 
1004
1071
  response = self._client._request("POST", "batch/documents", data=request)
1005
1072
  docs = [self._client._logic._parse_document_response(doc) for doc in response]
@@ -1007,12 +1074,17 @@ class UserScope:
1007
1074
  doc._client = self._client
1008
1075
  return docs
1009
1076
 
1010
- def batch_get_chunks(self, sources: List[Union[ChunkSource, Dict[str, Any]]]) -> List[FinalChunkResult]:
1077
+ def batch_get_chunks(
1078
+ self,
1079
+ sources: List[Union[ChunkSource, Dict[str, Any]]],
1080
+ additional_folders: Optional[List[str]] = None,
1081
+ ) -> List[FinalChunkResult]:
1011
1082
  """
1012
1083
  Retrieve specific chunks by their document ID and chunk number in a single batch operation for this end user.
1013
1084
 
1014
1085
  Args:
1015
1086
  sources: List of ChunkSource objects or dictionaries with document_id and chunk_number
1087
+ additional_folders: Optional list of extra folders to include in the scope
1016
1088
 
1017
1089
  Returns:
1018
1090
  List[FinalChunkResult]: List of chunk results
@@ -1025,12 +1097,11 @@ class UserScope:
1025
1097
  else:
1026
1098
  source_dicts.append(source.model_dump())
1027
1099
 
1028
- # Add end_user_id and folder_name to request
1100
+ merged = self._merge_folders(additional_folders)
1029
1101
  request = {"sources": source_dicts, "end_user_id": self._end_user_id}
1030
1102
 
1031
- # Add folder name if scoped to a folder
1032
- if self._folder_name:
1033
- request["folder_name"] = self._folder_name
1103
+ if merged:
1104
+ request["folder_name"] = merged
1034
1105
 
1035
1106
  response = self._client._request("POST", "batch/chunks", data=request)
1036
1107
  return self._client._logic._parse_chunk_result_list_response(response)
@@ -1071,7 +1142,9 @@ class UserScope:
1071
1142
  request["folder_name"] = self._folder_name
1072
1143
 
1073
1144
  response = self._client._request("POST", "graph/create", request)
1074
- return self._client._logic._parse_graph_response(response)
1145
+ graph = self._logic._parse_graph_response(response)
1146
+ graph._client = self
1147
+ return graph
1075
1148
 
1076
1149
  def update_graph(
1077
1150
  self,
@@ -1108,7 +1181,9 @@ class UserScope:
1108
1181
  request["folder_name"] = self._folder_name
1109
1182
 
1110
1183
  response = self._client._request("POST", f"graph/{name}/update", request)
1111
- return self._client._logic._parse_graph_response(response)
1184
+ graph = self._logic._parse_graph_response(response)
1185
+ graph._client = self
1186
+ return graph
1112
1187
 
1113
1188
  def delete_document_by_filename(self, filename: str) -> Dict[str, str]:
1114
1189
  """
@@ -1134,6 +1209,22 @@ class UserScope:
1134
1209
  # Then delete by ID
1135
1210
  return self._client.delete_document(doc.external_id)
1136
1211
 
1212
+ # Helper --------------------------------------------------------------
1213
+ def _merge_folders(self, additional_folders: Optional[List[str]] = None) -> Union[str, List[str], None]:
1214
+ """Return combined folder scope for user.
1215
+
1216
+ When this user scope is already tied to *self._folder_name* we combine it
1217
+ with any *additional_folders* passed by the caller. Otherwise just the
1218
+ *additional_folders* (or None) is returned so that upstream logic is
1219
+ unchanged.
1220
+ """
1221
+ base = self._folder_name
1222
+ if additional_folders:
1223
+ if base:
1224
+ return [base] + additional_folders
1225
+ return additional_folders
1226
+ return base
1227
+
1137
1228
 
1138
1229
  class Morphik:
1139
1230
  """
@@ -1542,6 +1633,7 @@ class Morphik:
1542
1633
  k: int = 4,
1543
1634
  min_score: float = 0.0,
1544
1635
  use_colpali: bool = True,
1636
+ folder_name: Optional[Union[str, List[str]]] = None,
1545
1637
  ) -> List[FinalChunkResult]:
1546
1638
  """
1547
1639
  Retrieve relevant chunks.
@@ -1564,7 +1656,9 @@ class Morphik:
1564
1656
  )
1565
1657
  ```
1566
1658
  """
1567
- payload = self._logic._prepare_retrieve_chunks_request(query, filters, k, min_score, use_colpali, None, None)
1659
+ payload = self._logic._prepare_retrieve_chunks_request(
1660
+ query, filters, k, min_score, use_colpali, folder_name, None
1661
+ )
1568
1662
  response = self._request("POST", "retrieve/chunks", data=payload)
1569
1663
  return self._logic._parse_chunk_result_list_response(response)
1570
1664
 
@@ -1575,6 +1669,7 @@ class Morphik:
1575
1669
  k: int = 4,
1576
1670
  min_score: float = 0.0,
1577
1671
  use_colpali: bool = True,
1672
+ folder_name: Optional[Union[str, List[str]]] = None,
1578
1673
  ) -> List[DocumentResult]:
1579
1674
  """
1580
1675
  Retrieve relevant documents.
@@ -1597,7 +1692,9 @@ class Morphik:
1597
1692
  )
1598
1693
  ```
1599
1694
  """
1600
- payload = self._logic._prepare_retrieve_docs_request(query, filters, k, min_score, use_colpali, None, None)
1695
+ payload = self._logic._prepare_retrieve_docs_request(
1696
+ query, filters, k, min_score, use_colpali, folder_name, None
1697
+ )
1601
1698
  response = self._request("POST", "retrieve/docs", data=payload)
1602
1699
  return self._logic._parse_document_result_list_response(response)
1603
1700
 
@@ -1614,6 +1711,7 @@ class Morphik:
1614
1711
  hop_depth: int = 1,
1615
1712
  include_paths: bool = False,
1616
1713
  prompt_overrides: Optional[Union[QueryPromptOverrides, Dict[str, Any]]] = None,
1714
+ folder_name: Optional[Union[str, List[str]]] = None,
1617
1715
  schema: Optional[Union[Type[BaseModel], Dict[str, Any]]] = None,
1618
1716
  ) -> CompletionResponse:
1619
1717
  """
@@ -1633,6 +1731,7 @@ class Morphik:
1633
1731
  include_paths: Whether to include relationship paths in the response
1634
1732
  prompt_overrides: Optional customizations for entity extraction, resolution, and query prompts
1635
1733
  Either a QueryPromptOverrides object or a dictionary with the same structure
1734
+ folder_name: Optional folder name to further scope operations
1636
1735
  schema: Optional schema for structured output, can be a Pydantic model or a JSON schema dict
1637
1736
  Returns:
1638
1737
  CompletionResponse
@@ -1704,6 +1803,7 @@ class Morphik:
1704
1803
  print(f"- {evidence}")
1705
1804
  ```
1706
1805
  """
1806
+ # Directly forward the supplied folder_name (may be None, str, or List[str])
1707
1807
  payload = self._logic._prepare_query_request(
1708
1808
  query,
1709
1809
  filters,
@@ -1716,8 +1816,8 @@ class Morphik:
1716
1816
  hop_depth,
1717
1817
  include_paths,
1718
1818
  prompt_overrides,
1719
- None,
1720
- None,
1819
+ folder_name,
1820
+ None, # end_user_id not supported at this level
1721
1821
  schema,
1722
1822
  )
1723
1823
 
@@ -1736,7 +1836,11 @@ class Morphik:
1736
1836
  return self._logic._parse_completion_response(response)
1737
1837
 
1738
1838
  def list_documents(
1739
- self, skip: int = 0, limit: int = 100, filters: Optional[Dict[str, Any]] = None
1839
+ self,
1840
+ skip: int = 0,
1841
+ limit: int = 100,
1842
+ filters: Optional[Dict[str, Any]] = None,
1843
+ folder_name: Optional[Union[str, List[str]]] = None,
1740
1844
  ) -> List[Document]:
1741
1845
  """
1742
1846
  List accessible documents.
@@ -1745,6 +1849,7 @@ class Morphik:
1745
1849
  skip: Number of documents to skip
1746
1850
  limit: Maximum number of documents to return
1747
1851
  filters: Optional filters
1852
+ folder_name: Optional folder name (or list of names) to scope the request
1748
1853
 
1749
1854
  Returns:
1750
1855
  List[Document]: List of accessible documents
@@ -1758,7 +1863,7 @@ class Morphik:
1758
1863
  next_page = db.list_documents(skip=10, limit=10, filters={"department": "research"})
1759
1864
  ```
1760
1865
  """
1761
- params, data = self._logic._prepare_list_documents_request(skip, limit, filters, None, None)
1866
+ params, data = self._logic._prepare_list_documents_request(skip, limit, filters, folder_name, None)
1762
1867
  response = self._request("POST", "documents", data=data, params=params)
1763
1868
  docs = self._logic._parse_document_list_response(response)
1764
1869
  for doc in docs:
@@ -2210,12 +2315,15 @@ class Morphik:
2210
2315
 
2211
2316
  return result
2212
2317
 
2213
- def batch_get_documents(self, document_ids: List[str]) -> List[Document]:
2318
+ def batch_get_documents(
2319
+ self, document_ids: List[str], folder_name: Optional[Union[str, List[str]]] = None
2320
+ ) -> List[Document]:
2214
2321
  """
2215
- Retrieve multiple documents by their IDs in a single batch operation.
2322
+ Retrieve multiple documents by their IDs.
2216
2323
 
2217
2324
  Args:
2218
2325
  document_ids: List of document IDs to retrieve
2326
+ folder_name: Optional folder name (or list of names) to scope the request
2219
2327
 
2220
2328
  Returns:
2221
2329
  List[Document]: List of document metadata for found documents
@@ -2227,19 +2335,23 @@ class Morphik:
2227
2335
  print(f"Document {doc.external_id}: {doc.metadata.get('title')}")
2228
2336
  ```
2229
2337
  """
2230
- # API expects a dict with document_ids key, not a direct list
2231
- response = self._request("POST", "batch/documents", data={"document_ids": document_ids})
2338
+ # Build request respecting folder scoping if provided
2339
+ request = self._logic._prepare_batch_get_documents_request(document_ids, folder_name, None)
2340
+ response = self._request("POST", "batch/documents", data=request)
2232
2341
  docs = self._logic._parse_document_list_response(response)
2233
2342
  for doc in docs:
2234
2343
  doc._client = self
2235
2344
  return docs
2236
2345
 
2237
- def batch_get_chunks(self, sources: List[Union[ChunkSource, Dict[str, Any]]]) -> List[FinalChunkResult]:
2346
+ def batch_get_chunks(
2347
+ self, sources: List[Union[ChunkSource, Dict[str, Any]]], folder_name: Optional[Union[str, List[str]]] = None
2348
+ ) -> List[FinalChunkResult]:
2238
2349
  """
2239
- Retrieve specific chunks by their document ID and chunk number in a single batch operation.
2350
+ Retrieve specific chunks by their document ID and chunk number.
2240
2351
 
2241
2352
  Args:
2242
2353
  sources: List of ChunkSource objects or dictionaries with document_id and chunk_number
2354
+ folder_name: Optional folder name (or list of names) to scope the request
2243
2355
 
2244
2356
  Returns:
2245
2357
  List[FinalChunkResult]: List of chunk results
@@ -2264,15 +2376,8 @@ class Morphik:
2264
2376
  print(f"Chunk from {chunk.document_id}, number {chunk.chunk_number}: {chunk.content[:50]}...")
2265
2377
  ```
2266
2378
  """
2267
- # Convert to list of dictionaries if needed
2268
- source_dicts = []
2269
- for source in sources:
2270
- if isinstance(source, dict):
2271
- source_dicts.append(source)
2272
- else:
2273
- source_dicts.append(source.model_dump())
2274
-
2275
- response = self._request("POST", "batch/chunks", data=source_dicts)
2379
+ request = self._logic._prepare_batch_get_chunks_request(sources, folder_name, None)
2380
+ response = self._request("POST", "batch/chunks", data=request)
2276
2381
  return self._logic._parse_chunk_result_list_response(response)
2277
2382
 
2278
2383
  def create_cache(
@@ -2410,7 +2515,9 @@ class Morphik:
2410
2515
  request["prompt_overrides"] = prompt_overrides
2411
2516
 
2412
2517
  response = self._request("POST", "graph/create", request)
2413
- return self._logic._parse_graph_response(response)
2518
+ graph = self._logic._parse_graph_response(response)
2519
+ graph._client = self
2520
+ return graph
2414
2521
 
2415
2522
  def get_graph(self, name: str) -> Graph:
2416
2523
  """
@@ -2430,7 +2537,9 @@ class Morphik:
2430
2537
  ```
2431
2538
  """
2432
2539
  response = self._request("GET", f"graph/{name}")
2433
- return self._logic._parse_graph_response(response)
2540
+ graph = self._logic._parse_graph_response(response)
2541
+ graph._client = self
2542
+ return graph
2434
2543
 
2435
2544
  def list_graphs(self) -> List[Graph]:
2436
2545
  """
@@ -2448,7 +2557,10 @@ class Morphik:
2448
2557
  ```
2449
2558
  """
2450
2559
  response = self._request("GET", "graphs")
2451
- return self._logic._parse_graph_list_response(response)
2560
+ graphs = self._logic._parse_graph_list_response(response)
2561
+ for g in graphs:
2562
+ g._client = self
2563
+ return graphs
2452
2564
 
2453
2565
  def update_graph(
2454
2566
  self,
@@ -2512,7 +2624,9 @@ class Morphik:
2512
2624
  }
2513
2625
 
2514
2626
  response = self._request("POST", f"graph/{name}/update", request)
2515
- return self._logic._parse_graph_response(response)
2627
+ graph = self._logic._parse_graph_response(response)
2628
+ graph._client = self
2629
+ return graph
2516
2630
 
2517
2631
  def delete_document(self, document_id: str) -> Dict[str, str]:
2518
2632
  """
@@ -2574,3 +2688,50 @@ class Morphik:
2574
2688
 
2575
2689
  def __exit__(self, exc_type, exc_val, exc_tb):
2576
2690
  self.close()
2691
+
2692
+ def create_app(self, app_id: str, name: str, expiry_days: int = 30) -> Dict[str, str]:
2693
+ """Create a new application in Morphik Cloud and obtain its auth URI.
2694
+
2695
+ This wraps the enterprise endpoint ``/ee/create_app`` which
2696
+ returns a dictionary ``{\"uri\": ..., \"app_id\": ...}``.
2697
+
2698
+ Parameters
2699
+ ----------
2700
+ app_id:
2701
+ Identifier for the new application.
2702
+ name:
2703
+ Human-readable application name (will be slugified by the server).
2704
+ expiry_days:
2705
+ Token validity period. Defaults to 30 days.
2706
+ """
2707
+
2708
+ payload = {"app_id": app_id, "name": name, "expiry_days": expiry_days}
2709
+ return self._request("POST", "ee/create_app", data=payload)
2710
+
2711
+ def wait_for_graph_completion(
2712
+ self,
2713
+ graph_name: str,
2714
+ timeout_seconds: int = 300,
2715
+ check_interval_seconds: int = 5,
2716
+ ) -> Graph:
2717
+ """Block until the specified graph finishes processing.
2718
+
2719
+ Args:
2720
+ graph_name: Name of the graph to monitor.
2721
+ timeout_seconds: Maximum seconds to wait.
2722
+ check_interval_seconds: Seconds between status checks.
2723
+
2724
+ Returns:
2725
+ Graph: The completed graph object.
2726
+ """
2727
+ import time
2728
+
2729
+ start = time.time()
2730
+ while time.time() - start < timeout_seconds:
2731
+ graph = self.get_graph(graph_name)
2732
+ if graph.is_completed:
2733
+ return graph
2734
+ if graph.is_failed:
2735
+ raise RuntimeError(graph.error or "Graph processing failed")
2736
+ time.sleep(check_interval_seconds)
2737
+ raise TimeoutError("Timed out waiting for graph completion")