sf-vector-sdk 0.2.0__py3-none-any.whl → 0.2.3__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.
@@ -12,6 +12,7 @@ from ..hash import (
12
12
  AudioRecapSectionData,
13
13
  FlashCardData,
14
14
  ToolCollection,
15
+ TopicData,
15
16
  compute_content_hash,
16
17
  extract_tool_text,
17
18
  )
@@ -47,6 +48,29 @@ class ToolMetadata:
47
48
  return result
48
49
 
49
50
 
51
+ @dataclass
52
+ class TopicMetadata:
53
+ """
54
+ Metadata for topic embeddings.
55
+ Unlike ToolMetadata, all fields are optional since topics don't have a toolId.
56
+ """
57
+
58
+ user_id: Optional[str] = None
59
+ topic_id: Optional[str] = None
60
+ extra: Optional[dict[str, Any]] = None
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ """Convert to dictionary for document storage."""
64
+ result: dict[str, Any] = {}
65
+ if self.user_id:
66
+ result["userId"] = self.user_id
67
+ if self.topic_id:
68
+ result["topicId"] = self.topic_id
69
+ if self.extra:
70
+ result.update(self.extra)
71
+ return result
72
+
73
+
50
74
  @dataclass
51
75
  class TestQuestionInput:
52
76
  """Extended question data with question type."""
@@ -67,6 +91,46 @@ class TestQuestionInput:
67
91
  return result
68
92
 
69
93
 
94
+ @dataclass
95
+ class BatchItem:
96
+ """Batch item for embedding multiple items of the same type."""
97
+
98
+ data: dict[str, Any]
99
+ metadata: ToolMetadata
100
+
101
+
102
+ @dataclass
103
+ class FlashCardBatchItem:
104
+ """Batch item for FlashCard embeddings."""
105
+
106
+ data: FlashCardData
107
+ metadata: ToolMetadata
108
+
109
+
110
+ @dataclass
111
+ class TestQuestionBatchItem:
112
+ """Batch item for TestQuestion embeddings."""
113
+
114
+ data: TestQuestionInput
115
+ metadata: ToolMetadata
116
+
117
+
118
+ @dataclass
119
+ class AudioRecapBatchItem:
120
+ """Batch item for AudioRecap embeddings."""
121
+
122
+ data: AudioRecapSectionData
123
+ metadata: ToolMetadata
124
+
125
+
126
+ @dataclass
127
+ class TopicBatchItem:
128
+ """Batch item for Topic embeddings."""
129
+
130
+ data: TopicData
131
+ metadata: TopicMetadata
132
+
133
+
70
134
  # ============================================================================
71
135
  # StructuredEmbeddingsNamespace
72
136
  # ============================================================================
@@ -150,6 +214,61 @@ class StructuredEmbeddingsNamespace(BaseNamespace):
150
214
  card_type = data.get("type")
151
215
  return self._embed_tool_and_wait("FlashCard", data, metadata, card_type, timeout)
152
216
 
217
+ def embed_flashcard_batch(
218
+ self,
219
+ items: list[FlashCardBatchItem],
220
+ ) -> str:
221
+ """
222
+ Embed a batch of flashcards and return the request ID.
223
+ All flashcards in the batch should have the same type for proper namespace routing.
224
+
225
+ Args:
226
+ items: List of FlashCardBatchItem objects
227
+
228
+ Returns:
229
+ The request ID
230
+ """
231
+ return self._embed_tool_batch(
232
+ "FlashCard",
233
+ [
234
+ {
235
+ "data": item.data,
236
+ "metadata": item.metadata,
237
+ "sub_type": item.data.get("type") if isinstance(item.data, dict) else None,
238
+ }
239
+ for item in items
240
+ ],
241
+ )
242
+
243
+ def embed_flashcard_batch_and_wait(
244
+ self,
245
+ items: list[FlashCardBatchItem],
246
+ timeout: int = 60,
247
+ ) -> EmbeddingResult:
248
+ """
249
+ Embed a batch of flashcards and wait for the result.
250
+ All flashcards in the batch should have the same type for proper namespace routing.
251
+
252
+ Args:
253
+ items: List of FlashCardBatchItem objects
254
+ timeout: Timeout in seconds (default: 60)
255
+
256
+ Returns:
257
+ The embedding result
258
+ """
259
+ return self._embed_tool_batch_and_wait(
260
+ "FlashCard",
261
+ [
262
+ {
263
+ "data": item.data,
264
+ "metadata": item.metadata,
265
+ "sub_type": item.data.get("type") if isinstance(item.data, dict) else None,
266
+ }
267
+ for item in items
268
+ ],
269
+ timeout,
270
+ )
271
+
153
272
  # ==========================================================================
154
273
  # TestQuestion Methods
155
274
  # ==========================================================================
@@ -201,6 +320,61 @@ class StructuredEmbeddingsNamespace(BaseNamespace):
201
320
  timeout,
202
321
  )
203
322
 
323
+ def embed_test_question_batch(
324
+ self,
325
+ items: list[TestQuestionBatchItem],
326
+ ) -> str:
327
+ """
328
+ Embed a batch of test questions and return the request ID.
329
+ All questions in the batch should have the same question_type for proper namespace routing.
330
+
331
+ Args:
332
+ items: List of TestQuestionBatchItem objects
333
+
334
+ Returns:
335
+ The request ID
336
+ """
337
+ return self._embed_tool_batch(
338
+ "TestQuestion",
339
+ [
340
+ {
341
+ "data": item.data.to_question_data(),
342
+ "metadata": item.metadata,
343
+ "sub_type": item.data.question_type,
344
+ }
345
+ for item in items
346
+ ],
347
+ )
348
+
349
+ def embed_test_question_batch_and_wait(
350
+ self,
351
+ items: list[TestQuestionBatchItem],
352
+ timeout: int = 60,
353
+ ) -> EmbeddingResult:
354
+ """
355
+ Embed a batch of test questions and wait for the result.
356
+ All questions in the batch should have the same question_type for proper namespace routing.
357
+
358
+ Args:
359
+ items: List of TestQuestionBatchItem objects
360
+ timeout: Timeout in seconds (default: 60)
361
+
362
+ Returns:
363
+ The embedding result
364
+ """
365
+ return self._embed_tool_batch_and_wait(
366
+ "TestQuestion",
367
+ [
368
+ {
369
+ "data": item.data.to_question_data(),
370
+ "metadata": item.metadata,
371
+ "sub_type": item.data.question_type,
372
+ }
373
+ for item in items
374
+ ],
375
+ timeout,
376
+ )
377
+
204
378
  # ==========================================================================
205
379
  # SpacedTestQuestion Methods
206
380
  # ==========================================================================
@@ -252,6 +426,61 @@ class StructuredEmbeddingsNamespace(BaseNamespace):
252
426
  timeout,
253
427
  )
254
428
 
429
+ def embed_spaced_test_question_batch(
430
+ self,
431
+ items: list[TestQuestionBatchItem],
432
+ ) -> str:
433
+ """
434
+ Embed a batch of spaced test questions and return the request ID.
435
+ All questions in the batch should have the same question_type for proper namespace routing.
436
+
437
+ Args:
438
+ items: List of TestQuestionBatchItem objects
439
+
440
+ Returns:
441
+ The request ID
442
+ """
443
+ return self._embed_tool_batch(
444
+ "SpacedTestQuestion",
445
+ [
446
+ {
447
+ "data": item.data.to_question_data(),
448
+ "metadata": item.metadata,
449
+ "sub_type": item.data.question_type,
450
+ }
451
+ for item in items
452
+ ],
453
+ )
454
+
455
+ def embed_spaced_test_question_batch_and_wait(
456
+ self,
457
+ items: list[TestQuestionBatchItem],
458
+ timeout: int = 60,
459
+ ) -> EmbeddingResult:
460
+ """
461
+ Embed a batch of spaced test questions and wait for the result.
462
+ All questions in the batch should have the same question_type for proper namespace routing.
463
+
464
+ Args:
465
+ items: List of TestQuestionBatchItem objects
466
+ timeout: Timeout in seconds (default: 60)
467
+
468
+ Returns:
469
+ The embedding result
470
+ """
471
+ return self._embed_tool_batch_and_wait(
472
+ "SpacedTestQuestion",
473
+ [
474
+ {
475
+ "data": item.data.to_question_data(),
476
+ "metadata": item.metadata,
477
+ "sub_type": item.data.question_type,
478
+ }
479
+ for item in items
480
+ ],
481
+ timeout,
482
+ )
483
+
255
484
  # ==========================================================================
256
485
  # AudioRecap Methods
257
486
  # ==========================================================================
@@ -298,6 +527,401 @@ class StructuredEmbeddingsNamespace(BaseNamespace):
298
527
  timeout,
299
528
  )
300
529
 
530
+ def embed_audio_recap_batch(
531
+ self,
532
+ items: list[AudioRecapBatchItem],
533
+ ) -> str:
534
+ """
535
+ Embed a batch of audio recap sections and return the request ID.
536
+
537
+ Args:
538
+ items: List of AudioRecapBatchItem objects
539
+
540
+ Returns:
541
+ The request ID
542
+ """
543
+ return self._embed_tool_batch(
544
+ "AudioRecapV2Section",
545
+ [
546
+ {
547
+ "data": item.data,
548
+ "metadata": item.metadata,
549
+ "sub_type": None,
550
+ }
551
+ for item in items
552
+ ],
553
+ )
554
+
555
+ def embed_audio_recap_batch_and_wait(
556
+ self,
557
+ items: list[AudioRecapBatchItem],
558
+ timeout: int = 60,
559
+ ) -> EmbeddingResult:
560
+ """
561
+ Embed a batch of audio recap sections and wait for the result.
562
+
563
+ Args:
564
+ items: List of AudioRecapBatchItem objects
565
+ timeout: Timeout in seconds (default: 60)
566
+
567
+ Returns:
568
+ The embedding result
569
+ """
570
+ return self._embed_tool_batch_and_wait(
571
+ "AudioRecapV2Section",
572
+ [
573
+ {
574
+ "data": item.data,
575
+ "metadata": item.metadata,
576
+ "sub_type": None,
577
+ }
578
+ for item in items
579
+ ],
580
+ timeout,
581
+ )
582
+
583
+ # ==========================================================================
584
+ # Topic Methods
585
+ # ==========================================================================
586
+
587
+ def embed_topic(
588
+ self,
589
+ data: TopicData,
590
+ metadata: TopicMetadata,
591
+ ) -> str:
592
+ """
593
+ Embed a topic and return the request ID.
594
+
595
+ Args:
596
+ data: Topic data (topic, description)
597
+ metadata: Topic metadata (all fields optional)
598
+
599
+ Returns:
600
+ The request ID
601
+ """
602
+ return self._embed_topic_internal("Topic", data, metadata, None)
603
+
604
+ def embed_topic_and_wait(
605
+ self,
606
+ data: TopicData,
607
+ metadata: TopicMetadata,
608
+ timeout: int = 60,
609
+ ) -> EmbeddingResult:
610
+ """
611
+ Embed a topic and wait for the result.
612
+
613
+ Args:
614
+ data: Topic data
615
+ metadata: Topic metadata (all fields optional)
616
+ timeout: Timeout in seconds (default: 60)
617
+
618
+ Returns:
619
+ The embedding result
620
+ """
621
+ return self._embed_topic_internal_and_wait("Topic", data, metadata, None, timeout)
622
+
623
+ def embed_topic_batch(
624
+ self,
625
+ items: list[TopicBatchItem],
626
+ ) -> str:
627
+ """
628
+ Embed a batch of topics and return the request ID.
629
+
630
+ Args:
631
+ items: List of TopicBatchItem objects
632
+
633
+ Returns:
634
+ The request ID
635
+ """
636
+ return self._embed_topic_batch_internal(items)
637
+
638
+ def embed_topic_batch_and_wait(
639
+ self,
640
+ items: list[TopicBatchItem],
641
+ timeout: int = 60,
642
+ ) -> EmbeddingResult:
643
+ """
644
+ Embed a batch of topics and wait for the result.
645
+
646
+ Args:
647
+ items: List of TopicBatchItem objects
648
+ timeout: Timeout in seconds (default: 60)
649
+
650
+ Returns:
651
+ The embedding result
652
+ """
653
+ return self._embed_topic_batch_internal_and_wait(items, timeout)
654
+
655
+ # ==========================================================================
656
+ # Internal Topic Methods (using TopicMetadata)
657
+ # ==========================================================================
658
+
659
+ def _embed_topic_internal(
660
+ self,
661
+ tool_collection: ToolCollection,
662
+ data: TopicData,
663
+ metadata: TopicMetadata,
664
+ sub_type: Optional[str],
665
+ ) -> str:
666
+ """Internal method to embed a topic with TopicMetadata."""
667
+ # 1. Extract text using the spec
668
+ text = extract_tool_text({"toolCollection": tool_collection, "data": data})
669
+ if not text:
670
+ raise ValueError(
671
+ f"Failed to extract text from {tool_collection} - empty content"
672
+ )
673
+
674
+ # 2. Compute content hash
675
+ content_hash = compute_content_hash(
676
+ {"toolCollection": tool_collection, "data": data}
677
+ )
678
+ if not content_hash:
679
+ raise ValueError(
680
+ f"Failed to compute content hash for {tool_collection} - empty content"
681
+ )
682
+
683
+ # 3. Get tool config
684
+ tool_config = get_tool_config(tool_collection)
685
+
686
+ # 4. Build document with metadata (TopicMetadata doesn't have toolId)
687
+ document = {
688
+ **metadata.to_dict(),
689
+ "toolCollection": tool_collection,
690
+ "contentHash": content_hash,
691
+ }
692
+
693
+ # 5. Build storage config using router
694
+ storage_config = build_storage_config(
695
+ tool_collection=tool_collection,
696
+ sub_type=sub_type,
697
+ content_hash=content_hash,
698
+ document_fields=document,
699
+ )
700
+
701
+ # 6. Build text input
702
+ text_input = {
703
+ "id": content_hash,
704
+ "text": text,
705
+ "document": document,
706
+ }
707
+
708
+ # 7. Submit using embeddings namespace
709
+ return self._embeddings.create(
710
+ texts=[text_input],
711
+ content_type=get_content_type(tool_collection),
712
+ priority=tool_config.default_priority,
713
+ storage=storage_config,
714
+ metadata={
715
+ "toolCollection": tool_collection,
716
+ "contentHash": content_hash,
717
+ },
718
+ embedding_model=tool_config.model,
719
+ embedding_dimensions=tool_config.dimensions,
720
+ )
721
+
722
+ def _embed_topic_internal_and_wait(
723
+ self,
724
+ tool_collection: ToolCollection,
725
+ data: TopicData,
726
+ metadata: TopicMetadata,
727
+ sub_type: Optional[str],
728
+ timeout: int = 60,
729
+ ) -> EmbeddingResult:
730
+ """Internal method to embed a topic and wait for result."""
731
+ # 1. Extract text using the spec
732
+ text = extract_tool_text({"toolCollection": tool_collection, "data": data})
733
+ if not text:
734
+ raise ValueError(
735
+ f"Failed to extract text from {tool_collection} - empty content"
736
+ )
737
+
738
+ # 2. Compute content hash
739
+ content_hash = compute_content_hash(
740
+ {"toolCollection": tool_collection, "data": data}
741
+ )
742
+ if not content_hash:
743
+ raise ValueError(
744
+ f"Failed to compute content hash for {tool_collection} - empty content"
745
+ )
746
+
747
+ # 3. Get tool config
748
+ tool_config = get_tool_config(tool_collection)
749
+
750
+ # 4. Build document with metadata
751
+ document = {
752
+ **metadata.to_dict(),
753
+ "toolCollection": tool_collection,
754
+ "contentHash": content_hash,
755
+ }
756
+
757
+ # 5. Build storage config using router
758
+ storage_config = build_storage_config(
759
+ tool_collection=tool_collection,
760
+ sub_type=sub_type,
761
+ content_hash=content_hash,
762
+ document_fields=document,
763
+ )
764
+
765
+ # 6. Build text input
766
+ text_input = {
767
+ "id": content_hash,
768
+ "text": text,
769
+ "document": document,
770
+ }
771
+
772
+ # 7. Submit and wait using embeddings namespace
773
+ return self._embeddings.create_and_wait(
774
+ texts=[text_input],
775
+ content_type=get_content_type(tool_collection),
776
+ priority=tool_config.default_priority,
777
+ storage=storage_config,
778
+ metadata={
779
+ "toolCollection": tool_collection,
780
+ "contentHash": content_hash,
781
+ },
782
+ embedding_model=tool_config.model,
783
+ embedding_dimensions=tool_config.dimensions,
784
+ timeout=timeout,
785
+ )
786
+
787
+ def _embed_topic_batch_internal(
788
+ self,
789
+ items: list[TopicBatchItem],
790
+ ) -> str:
791
+ """Internal method to embed a batch of topics."""
792
+ if not items:
793
+ raise ValueError("Batch cannot be empty")
794
+
795
+ tool_collection: ToolCollection = "Topic"
796
+ tool_config = get_tool_config(tool_collection)
797
+
798
+ # Process each item
799
+ text_inputs = []
800
+ for item in items:
801
+ data = item.data
802
+ metadata = item.metadata
803
+
804
+ # Extract text
805
+ text = extract_tool_text({"toolCollection": tool_collection, "data": data})
806
+ if not text:
807
+ raise ValueError(
808
+ f"Failed to extract text from {tool_collection} - empty content"
809
+ )
810
+
811
+ # Compute content hash
812
+ content_hash = compute_content_hash(
813
+ {"toolCollection": tool_collection, "data": data}
814
+ )
815
+ if not content_hash:
816
+ raise ValueError(
817
+ f"Failed to compute content hash for {tool_collection} - empty content"
818
+ )
819
+
820
+ # Build document with metadata (TopicMetadata doesn't have toolId)
821
+ document = {
822
+ **metadata.to_dict(),
823
+ "toolCollection": tool_collection,
824
+ "contentHash": content_hash,
825
+ }
826
+
827
+ text_inputs.append({
828
+ "id": content_hash,
829
+ "text": text,
830
+ "document": document,
831
+ })
832
+
833
+ # Build storage config using first item
834
+ storage_config = build_storage_config(
835
+ tool_collection=tool_collection,
836
+ sub_type=None,
837
+ content_hash=text_inputs[0]["id"],
838
+ document_fields=text_inputs[0]["document"],
839
+ )
840
+
841
+ # Submit batch to embeddings namespace
842
+ return self._embeddings.create(
843
+ texts=text_inputs,
844
+ content_type=get_content_type(tool_collection),
845
+ priority=tool_config.default_priority,
846
+ storage=storage_config,
847
+ metadata={
848
+ "toolCollection": tool_collection,
849
+ "batchSize": len(items),
850
+ },
851
+ embedding_model=tool_config.model,
852
+ embedding_dimensions=tool_config.dimensions,
853
+ )
854
+
855
+ def _embed_topic_batch_internal_and_wait(
856
+ self,
857
+ items: list[TopicBatchItem],
858
+ timeout: int = 60,
859
+ ) -> EmbeddingResult:
860
+ """Internal method to embed a batch of topics and wait for result."""
861
+ if not items:
862
+ raise ValueError("Batch cannot be empty")
863
+
864
+ tool_collection: ToolCollection = "Topic"
865
+ tool_config = get_tool_config(tool_collection)
866
+
867
+ # Process each item
868
+ text_inputs = []
869
+ for item in items:
870
+ data = item.data
871
+ metadata = item.metadata
872
+
873
+ # Extract text
874
+ text = extract_tool_text({"toolCollection": tool_collection, "data": data})
875
+ if not text:
876
+ raise ValueError(
877
+ f"Failed to extract text from {tool_collection} - empty content"
878
+ )
879
+
880
+ # Compute content hash
881
+ content_hash = compute_content_hash(
882
+ {"toolCollection": tool_collection, "data": data}
883
+ )
884
+ if not content_hash:
885
+ raise ValueError(
886
+ f"Failed to compute content hash for {tool_collection} - empty content"
887
+ )
888
+
889
+ # Build document with metadata
890
+ document = {
891
+ **metadata.to_dict(),
892
+ "toolCollection": tool_collection,
893
+ "contentHash": content_hash,
894
+ }
895
+
896
+ text_inputs.append({
897
+ "id": content_hash,
898
+ "text": text,
899
+ "document": document,
900
+ })
901
+
902
+ # Build storage config using first item
903
+ storage_config = build_storage_config(
904
+ tool_collection=tool_collection,
905
+ sub_type=None,
906
+ content_hash=text_inputs[0]["id"],
907
+ document_fields=text_inputs[0]["document"],
908
+ )
909
+
910
+ # Submit batch and wait
911
+ return self._embeddings.create_and_wait(
912
+ texts=text_inputs,
913
+ content_type=get_content_type(tool_collection),
914
+ priority=tool_config.default_priority,
915
+ storage=storage_config,
916
+ metadata={
917
+ "toolCollection": tool_collection,
918
+ "batchSize": len(items),
919
+ },
920
+ embedding_model=tool_config.model,
921
+ embedding_dimensions=tool_config.dimensions,
922
+ timeout=timeout,
923
+ )
924
+
301
925
  # ==========================================================================
302
926
  # Internal Methods
303
927
  # ==========================================================================
@@ -429,3 +1053,164 @@ class StructuredEmbeddingsNamespace(BaseNamespace):
429
1053
  embedding_dimensions=tool_config.dimensions,
430
1054
  timeout=timeout,
431
1055
  )
1056
+
1057
+ def _embed_tool_batch(
1058
+ self,
1059
+ tool_collection: ToolCollection,
1060
+ items: list[dict[str, Any]],
1061
+ ) -> str:
1062
+ """
1063
+ Internal method to embed a batch of items of the same tool type.
1064
+
1065
+ Args:
1066
+ tool_collection: The tool collection type
1067
+ items: List of dicts with 'data', 'metadata', and optional 'sub_type' keys
1068
+
1069
+ Returns:
1070
+ The request ID
1071
+ """
1072
+ if not items:
1073
+ raise ValueError("Batch cannot be empty")
1074
+
1075
+ # Get tool config (same for all items)
1076
+ tool_config = get_tool_config(tool_collection)
1077
+
1078
+ # Process each item
1079
+ text_inputs = []
1080
+ for item in items:
1081
+ data = item["data"]
1082
+ metadata = item["metadata"]
1083
+
1084
+ # Extract text
1085
+ text = extract_tool_text({"toolCollection": tool_collection, "data": data})
1086
+ if not text:
1087
+ raise ValueError(
1088
+ f"Failed to extract text from {tool_collection} - empty content"
1089
+ )
1090
+
1091
+ # Compute content hash
1092
+ content_hash = compute_content_hash(
1093
+ {"toolCollection": tool_collection, "data": data}
1094
+ )
1095
+ if not content_hash:
1096
+ raise ValueError(
1097
+ f"Failed to compute content hash for {tool_collection} - empty content"
1098
+ )
1099
+
1100
+ # Build document with metadata
1101
+ document = {
1102
+ **metadata.to_dict(),
1103
+ "toolCollection": tool_collection,
1104
+ "contentHash": content_hash,
1105
+ }
1106
+
1107
+ text_inputs.append({
1108
+ "id": content_hash,
1109
+ "text": text,
1110
+ "document": document,
1111
+ })
1112
+
1113
+ # Build storage config using first item's sub_type
1114
+ first_item = items[0]
1115
+ storage_config = build_storage_config(
1116
+ tool_collection=tool_collection,
1117
+ sub_type=first_item.get("sub_type"),
1118
+ content_hash=text_inputs[0]["id"],
1119
+ document_fields=text_inputs[0]["document"],
1120
+ )
1121
+
1122
+ # Submit batch to embeddings namespace
1123
+ return self._embeddings.create(
1124
+ texts=text_inputs,
1125
+ content_type=get_content_type(tool_collection),
1126
+ priority=tool_config.default_priority,
1127
+ storage=storage_config,
1128
+ metadata={
1129
+ "toolCollection": tool_collection,
1130
+ "batchSize": len(items),
1131
+ },
1132
+ embedding_model=tool_config.model,
1133
+ embedding_dimensions=tool_config.dimensions,
1134
+ )
1135
+
1136
+ def _embed_tool_batch_and_wait(
1137
+ self,
1138
+ tool_collection: ToolCollection,
1139
+ items: list[dict[str, Any]],
1140
+ timeout: int = 60,
1141
+ ) -> EmbeddingResult:
1142
+ """
1143
+ Internal method to embed a batch of items and wait for result.
1144
+
1145
+ Args:
1146
+ tool_collection: The tool collection type
1147
+ items: List of dicts with 'data', 'metadata', and optional 'sub_type' keys
1148
+ timeout: Timeout in seconds (default: 60)
1149
+
1150
+ Returns:
1151
+ The embedding result
1152
+ """
1153
+ if not items:
1154
+ raise ValueError("Batch cannot be empty")
1155
+
1156
+ # Get tool config (same for all items)
1157
+ tool_config = get_tool_config(tool_collection)
1158
+
1159
+ # Process each item
1160
+ text_inputs = []
1161
+ for item in items:
1162
+ data = item["data"]
1163
+ metadata = item["metadata"]
1164
+
1165
+ # Extract text
1166
+ text = extract_tool_text({"toolCollection": tool_collection, "data": data})
1167
+ if not text:
1168
+ raise ValueError(
1169
+ f"Failed to extract text from {tool_collection} - empty content"
1170
+ )
1171
+
1172
+ # Compute content hash
1173
+ content_hash = compute_content_hash(
1174
+ {"toolCollection": tool_collection, "data": data}
1175
+ )
1176
+ if not content_hash:
1177
+ raise ValueError(
1178
+ f"Failed to compute content hash for {tool_collection} - empty content"
1179
+ )
1180
+
1181
+ # Build document with metadata
1182
+ document = {
1183
+ **metadata.to_dict(),
1184
+ "toolCollection": tool_collection,
1185
+ "contentHash": content_hash,
1186
+ }
1187
+
1188
+ text_inputs.append({
1189
+ "id": content_hash,
1190
+ "text": text,
1191
+ "document": document,
1192
+ })
1193
+
1194
+ # Build storage config using first item's sub_type
1195
+ first_item = items[0]
1196
+ storage_config = build_storage_config(
1197
+ tool_collection=tool_collection,
1198
+ sub_type=first_item.get("sub_type"),
1199
+ content_hash=text_inputs[0]["id"],
1200
+ document_fields=text_inputs[0]["document"],
1201
+ )
1202
+
1203
+ # Submit batch and wait
1204
+ return self._embeddings.create_and_wait(
1205
+ texts=text_inputs,
1206
+ content_type=get_content_type(tool_collection),
1207
+ priority=tool_config.default_priority,
1208
+ storage=storage_config,
1209
+ metadata={
1210
+ "toolCollection": tool_collection,
1211
+ "batchSize": len(items),
1212
+ },
1213
+ embedding_model=tool_config.model,
1214
+ embedding_dimensions=tool_config.dimensions,
1215
+ timeout=timeout,
1216
+ )