11package com.langchain.smith.client
22
3+ import com.fasterxml.jackson.databind.node.ObjectNode
34import com.langchain.smith.core.RequestOptions
45import com.langchain.smith.core.Timeout
56import com.langchain.smith.core.http.Headers
67import com.langchain.smith.core.http.QueryParams
8+ import com.langchain.smith.core.jsonMapper
79import com.langchain.smith.models.runs.Run
810import com.langchain.smith.models.runs.RunIngestBatchParams
911import java.util.concurrent.CompletionException
@@ -19,6 +21,7 @@ import java.util.concurrent.TimeUnit
1921import java.util.concurrent.atomic.AtomicBoolean
2022import java.util.concurrent.atomic.AtomicInteger
2123import java.util.concurrent.locks.ReentrantLock
24+ import kotlin.jvm.optionals.getOrNull
2225import org.slf4j.LoggerFactory
2326
2427/* *
@@ -238,9 +241,6 @@ class AutoBatchQueue(
238241 * Drains up to [maxItems] queued operations and returns batch params grouped by request
239242 * options.
240243 *
241- * TODO: Merge create + update for the same run ID before sending (like the JS/Python SDKs).
242- * This would reduce the number of operations in each batch when a run is created and
243- * immediately updated (common for short-lived runs).
244244 * TODO: Also flush/split batches based on serialized payload size, not just operation count.
245245 * TODO: Support multipart ingest endpoint for large payloads with attachments.
246246 * TODO: Support gzip compression for batch requests.
@@ -324,15 +324,58 @@ class AutoBatchQueue(
324324 val queryParams : QueryParams .Builder = QueryParams .builder(),
325325 ) {
326326 fun toBatch (): Batch {
327+ val mergeResult = mergePostsAndPatches()
327328 val builder = RunIngestBatchParams .builder()
328- if (posts.isNotEmpty()) builder.post(posts)
329- if (patches.isNotEmpty()) builder.patch(patches)
329+ if (mergeResult. posts.isNotEmpty()) builder.post(mergeResult. posts)
330+ if (mergeResult. patches.isNotEmpty()) builder.patch(mergeResult. patches)
330331 builder.additionalHeaders(headers.build())
331332 builder.additionalQueryParams(queryParams.build())
332333 return Batch (params = builder.build(), requestOptions = requestOptions)
333334 }
335+
336+ private fun mergePostsAndPatches (): MergeResult {
337+ if (posts.isEmpty() || patches.isEmpty()) {
338+ return MergeResult (posts = posts, patches = patches)
339+ }
340+
341+ val postsById =
342+ posts.mapNotNull { post -> post.id().getOrNull()?.let { it to post } }.toMap()
343+ val postsWithoutId = posts.filter { it.id().getOrNull() == null }
344+ val patchesByPostId =
345+ patches.mapNotNull { patch ->
346+ patch.id().getOrNull()?.takeIf (postsById::containsKey)?.let { it to patch }
347+ }
348+ val patchesByPostIdMap = patchesByPostId.toMap()
349+ val standalonePatches =
350+ patches.filter { patch ->
351+ patch.id().getOrNull()?.let (postsById::containsKey) != true
352+ }
353+
354+ return MergeResult (
355+ posts =
356+ postsWithoutId +
357+ postsById.map { (id, post) ->
358+ patchesByPostIdMap[id]?.let { mergePostAndPatch(post, it) } ? : post
359+ },
360+ patches = standalonePatches,
361+ mergedRunIds = patchesByPostId.map { it.first },
362+ )
363+ }
364+
365+ private fun mergePostAndPatch (post : Run , patch : Run ): Run {
366+ val merged = objectMapper.valueToTree<ObjectNode >(post)
367+ val patchFields = objectMapper.valueToTree<ObjectNode >(patch)
368+ patchFields.fields().forEach { (field, value) -> merged.set<ObjectNode >(field, value) }
369+ return objectMapper.treeToValue(merged, Run ::class .java)
370+ }
334371 }
335372
373+ private data class MergeResult (
374+ val posts : List <Run >,
375+ val patches : List <Run >,
376+ val mergedRunIds : List <String > = emptyList(),
377+ )
378+
336379 private data class BatchItem (
337380 val op : BatchOp ,
338381 val run : Run ,
@@ -348,6 +391,7 @@ class AutoBatchQueue(
348391
349392 companion object {
350393 private val logger = LoggerFactory .getLogger(AutoBatchQueue ::class .java)
394+ private val objectMapper = jsonMapper()
351395
352396 const val DEFAULT_BATCH_SIZE_LIMIT = 100
353397 const val DEFAULT_AGGREGATION_DELAY_MS = 250L
0 commit comments