Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public Flux<String> streamAnswerWithContext(
buildStructuredPromptWithContextAndGuidance(history, latestUserMessage, contextDocs, guidance);

return openAIStreamingService
.streamResponse(structuredPrompt, temperature)
.streamResponse(LlmGatewayTier.Purpose.CHAT_ANSWER, structuredPrompt, temperature)
.flatMapMany(streamingResult -> streamingResult.textChunks());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ final class EmbeddingBatchEmbedder {

private EmbeddingBatchEmbedder() {}

static List<float[]> embedDocuments(EmbeddingClient embeddingClient, List<Document> documents) {
static List<float[]> embedDocuments(
LlmGatewayTier.Purpose purpose, EmbeddingClient embeddingClient, List<Document> documents) {
Objects.requireNonNull(purpose, "purpose");
Objects.requireNonNull(embeddingClient, "embeddingClient");
Objects.requireNonNull(documents, "documents");

Expand All @@ -50,8 +52,8 @@ static List<float[]> embedDocuments(EmbeddingClient embeddingClient, List<Docume
int requestWaveEndIndex =
Math.min(requestWaveStartIndex + MAX_CONCURRENT_EMBEDDING_REQUESTS, embeddingRequestCount);
List<Future<List<float[]>>> embeddingWave = IntStream.range(currentWaveStartIndex, requestWaveEndIndex)
.mapToObj(requestIndex -> embeddingExecutor.submit(() ->
embedRequest(embeddingClient, documents, requestIndex, expectedEmbeddingDimensions)))
.mapToObj(requestIndex -> embeddingExecutor.submit(() -> embedRequest(
purpose, embeddingClient, documents, requestIndex, expectedEmbeddingDimensions)))
.toList();
for (Future<List<float[]>> embeddingRequest : embeddingWave) {
try {
Expand Down Expand Up @@ -79,13 +81,15 @@ static List<float[]> embedDocuments(EmbeddingClient embeddingClient, List<Docume
}

private static List<float[]> embedRequest(
LlmGatewayTier.Purpose purpose,
EmbeddingClient embeddingClient,
List<Document> documents,
int requestIndex,
int expectedEmbeddingDimensions) {
int batchStartIndex = requestIndex * EMBEDDING_REQUEST_BATCH_SIZE;
int batchEndIndex = Math.min(batchStartIndex + EMBEDDING_REQUEST_BATCH_SIZE, documents.size());
return embedSingleBatch(
purpose,
embeddingClient,
documents.subList(batchStartIndex, batchEndIndex),
batchStartIndex,
Expand All @@ -100,6 +104,7 @@ private static List<float[]> embedRequest(
* callers can identify which documents caused the failure.</p>
*/
private static List<float[]> embedSingleBatch(
LlmGatewayTier.Purpose purpose,
EmbeddingClient embeddingClient,
List<Document> documentBatch,
int batchStartIndex,
Expand All @@ -115,7 +120,7 @@ private static List<float[]> embedSingleBatch(

List<float[]> batchEmbeddings;
try {
batchEmbeddings = embedWithRetry(embeddingClient, textBatch);
batchEmbeddings = embedWithRetry(purpose, embeddingClient, textBatch);
} catch (EmbeddingServiceUnavailableException embeddingFailure) {
String firstBatchUrl = extractDocumentUrl(documentBatch.getFirst(), batchStartIndex);
String lastBatchUrl = extractDocumentUrl(documentBatch.getLast(), batchEndIndex - 1);
Expand Down Expand Up @@ -151,10 +156,11 @@ private static List<float[]> embedSingleBatch(
return batchEmbeddings;
}

private static List<float[]> embedWithRetry(EmbeddingClient embeddingClient, List<String> textBatch) {
private static List<float[]> embedWithRetry(
LlmGatewayTier.Purpose purpose, EmbeddingClient embeddingClient, List<String> textBatch) {
for (int embeddingAttempt = 1; embeddingAttempt <= MAX_EMBEDDING_ATTEMPTS; embeddingAttempt++) {
try {
return embeddingClient.embed(textBatch, LlmGatewayTier.BATCH);
return embeddingClient.embed(purpose, textBatch, LlmGatewayTier.BATCH);
} catch (EmbeddingServiceTemporarilyUnavailableException temporaryFailure) {
if (embeddingAttempt == MAX_EMBEDDING_ATTEMPTS) {
throw temporaryFailure;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ public interface EmbeddingClient {
/**
* Produces one dense embedding vector per input text, preserving input order.
*
* @param purpose application job responsible for the request
* @param texts input texts
* @param requestTier gateway capacity tier for this embedding request
* @return embedding vectors in the same order as {@code texts}
*/
List<float[]> embed(List<String> texts, LlmGatewayTier requestTier);
List<float[]> embed(LlmGatewayTier.Purpose purpose, List<String> texts, LlmGatewayTier requestTier);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bundle the embedding request parameters

This change makes purpose, texts, and requestTier a three-parameter group that is forwarded together through the default methods, implementations, batching layer, and callers. Introduce a typed embedding-request record instead of propagating this new data clump across every boundary.

AGENTS.md reference: AGENTS.md:L200-L200

Useful? React with 👍 / 👎.


/**
* Produces one dense embedding vector per input text within a caller-owned budget.
Expand All @@ -29,12 +30,14 @@ public interface EmbeddingClient {
* {@code requestTimeout} and the tier's configured request timeout, keeping the
* configured timeout meaningful as a per-request ceiling.</p>
*
* @param purpose application job responsible for the request
* @param texts input texts
* @param requestTier gateway capacity tier for this embedding request
* @param requestTimeout caller-owned budget bounding the whole embedding operation
* @return embedding vectors in the same order as {@code texts}
*/
List<float[]> embed(List<String> texts, LlmGatewayTier requestTier, Duration requestTimeout);
List<float[]> embed(
LlmGatewayTier.Purpose purpose, List<String> texts, LlmGatewayTier requestTier, Duration requestTimeout);

/**
* Returns the provider model identifier used for embedding requests.
Expand All @@ -46,14 +49,16 @@ public interface EmbeddingClient {
/**
* Produces a dense embedding vector for a single text.
*
* @param purpose application job responsible for the request
* @param text input text
* @param requestTier gateway capacity tier for this embedding request
* @return embedding vector
*/
default float[] embed(String text, LlmGatewayTier requestTier) {
default float[] embed(LlmGatewayTier.Purpose purpose, String text, LlmGatewayTier requestTier) {
Objects.requireNonNull(purpose, "purpose");
Objects.requireNonNull(requestTier, "requestTier");
String safeText = Objects.requireNonNullElse(text, "");
List<float[]> embeddingVectors = embed(List.of(safeText), requestTier);
List<float[]> embeddingVectors = embed(purpose, List.of(safeText), requestTier);
if (embeddingVectors.isEmpty()) {
throw new EmbeddingServiceUnavailableException("Embedding response was empty");
}
Expand All @@ -63,16 +68,19 @@ default float[] embed(String text, LlmGatewayTier requestTier) {
/**
* Produces a dense embedding vector for a single text within a caller-owned budget.
*
* @param purpose application job responsible for the request
* @param text input text
* @param requestTier gateway capacity tier for this embedding request
* @param requestTimeout caller-owned budget bounding the whole embedding operation
* @return embedding vector
*/
default float[] embed(String text, LlmGatewayTier requestTier, Duration requestTimeout) {
default float[] embed(
LlmGatewayTier.Purpose purpose, String text, LlmGatewayTier requestTier, Duration requestTimeout) {
Objects.requireNonNull(purpose, "purpose");
Objects.requireNonNull(requestTier, "requestTier");
Objects.requireNonNull(requestTimeout, "requestTimeout");
String safeText = Objects.requireNonNullElse(text, "");
List<float[]> embeddingVectors = embed(List.of(safeText), requestTier, requestTimeout);
List<float[]> embeddingVectors = embed(purpose, List.of(safeText), requestTier, requestTimeout);
if (embeddingVectors.isEmpty()) {
throw new EmbeddingServiceUnavailableException("Embedding response was empty");
}
Expand All @@ -90,7 +98,7 @@ default float[] embed(String text, LlmGatewayTier requestTier, Duration requestT
* Issues a minimal embedding request so the provider keeps its model resident.
*
* <p>Implementations must call their provider-specific request path directly instead
* of delegating to {@link #embed(List, LlmGatewayTier)}. The RAG pipeline logging aspect advises
* of delegating to {@link #embed(LlmGatewayTier.Purpose, List, LlmGatewayTier)}. The RAG pipeline logging aspect advises
* public {@code embed} executions, so routing scheduled probes around that method
* keeps "STEP 1" pipeline logs scoped to real requests.</p>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ public Enrichment enrich(String userQuery, String jdkVersion, List<String> conte
}

String enrichmentJson = openAIStreamingService
.completeJsonObject(prompt.toString(), enrichmentTemperature, enrichmentOutputTokenBudget)
.completeJsonObject(
LlmGatewayTier.Purpose.MARKDOWN_ENRICHMENT,
prompt.toString(),
enrichmentTemperature,
enrichmentOutputTokenBudget)
.block();
if (enrichmentJson == null || enrichmentJson.isBlank()) {
throw new IllegalStateException("LLM returned empty enrichment response");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ public List<List<Document>> searchByConstraint(

float[] denseVector = queryEncoding
.embeddingClient()
.embed(query, LlmGatewayTier.LIVE, remainingStageBudget(stageDeadlineNanos));
.embed(
LlmGatewayTier.Purpose.SEARCH_QUERY_EMBEDDING,
query,
LlmGatewayTier.LIVE,
remainingStageBudget(stageDeadlineNanos));
LexicalSparseVectorEncoder.SparseVector sparseVector =
queryEncoding.sparseVectorEncoder().encode(query);
Duration queryTimeout = qdrantQueryExecutor.queryTimeout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public void upsert(QdrantCollectionKind collectionKind, List<Document> documents
Objects.requireNonNull(collectionKind, NULL_MESSAGE_COLLECTION_KIND);
String collectionName =
Objects.requireNonNull(resolveCollectionName(collectionKind), NULL_MESSAGE_COLLECTION_NAME);
doUpsert(collectionName, documents);
doUpsert(indexingPurpose(collectionKind), collectionName, documents);
}

/**
Expand All @@ -109,7 +109,7 @@ public void upsertToCollection(String collectionName, List<Document> documents)
if (collectionName.isBlank()) {
throw new IllegalArgumentException("collectionName must not be blank");
}
doUpsert(collectionName, documents);
doUpsert(LlmGatewayTier.Purpose.REPOSITORY_INDEX_EMBEDDING, collectionName, documents);
}

/**
Expand All @@ -129,7 +129,7 @@ public void replaceUrlDocuments(
Objects.requireNonNull(collectionKind, NULL_MESSAGE_COLLECTION_KIND);
String collectionName =
Objects.requireNonNull(resolveCollectionName(collectionKind), NULL_MESSAGE_COLLECTION_NAME);
replaceUrlDocuments(collectionName, sourceUrl, replacementDocuments);
replaceUrlDocuments(indexingPurpose(collectionKind), collectionName, sourceUrl, replacementDocuments);
}

/**
Expand All @@ -140,6 +140,15 @@ public void replaceUrlDocuments(
* @param replacementDocuments complete replacement document set with stable UUID identities
*/
public void replaceUrlDocuments(String collectionName, String sourceUrl, List<Document> replacementDocuments) {
replaceUrlDocuments(
LlmGatewayTier.Purpose.REPOSITORY_INDEX_EMBEDDING, collectionName, sourceUrl, replacementDocuments);
}

private void replaceUrlDocuments(
LlmGatewayTier.Purpose purpose,
String collectionName,
String sourceUrl,
List<Document> replacementDocuments) {
Objects.requireNonNull(collectionName, NULL_MESSAGE_COLLECTION_NAME);
if (collectionName.isBlank()) {
throw new IllegalArgumentException("collectionName must not be blank");
Expand All @@ -158,7 +167,7 @@ public void replaceUrlDocuments(String collectionName, String sourceUrl, List<Do
Set<PointId> replacementPointIds = replacementPointIds(requiredReplacementDocuments);
Set<PointId> priorPointIds = scrollPointIdsForUrl(collectionName, requiredSourceUrl);

doUpsert(collectionName, requiredReplacementDocuments);
doUpsert(purpose, collectionName, requiredReplacementDocuments);

priorPointIds.removeAll(replacementPointIds);
deletePointIds(collectionName, priorPointIds);
Expand Down Expand Up @@ -448,7 +457,16 @@ public String resolveCollectionName(QdrantCollectionKind kind) {
};
}

private void doUpsert(String collectionName, List<Document> documents) {
private static LlmGatewayTier.Purpose indexingPurpose(QdrantCollectionKind collectionKind) {
return switch (collectionKind) {
case BOOKS -> LlmGatewayTier.Purpose.BOOK_INDEX_EMBEDDING;
case DOCS -> LlmGatewayTier.Purpose.DOCUMENTATION_INDEX_EMBEDDING;
case ARTICLES -> LlmGatewayTier.Purpose.ARTICLE_INDEX_EMBEDDING;
case PDFS -> LlmGatewayTier.Purpose.PDF_INDEX_EMBEDDING;
};
}

private void doUpsert(LlmGatewayTier.Purpose purpose, String collectionName, List<Document> documents) {
Objects.requireNonNull(documents, "documents");
if (documents.isEmpty()) {
return;
Expand All @@ -457,8 +475,8 @@ private void doUpsert(String collectionName, List<Document> documents) {
String denseVectorName = appProperties.getQdrant().getDenseVectorName();
String sparseVectorName = appProperties.getQdrant().getSparseVectorName();

List<float[]> embeddings =
Objects.requireNonNull(EmbeddingBatchEmbedder.embedDocuments(embeddingClient, documents), "embeddings");
List<float[]> embeddings = Objects.requireNonNull(
EmbeddingBatchEmbedder.embedDocuments(purpose, embeddingClient, documents), "embeddings");
if (embeddings.size() != documents.size()) {
throw new IllegalStateException(
"Embedding count mismatch: expected " + documents.size() + " but received " + embeddings.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,42 @@ public String requestHeader() {
}

/** Returns the stable Java Chat workload label for this request tier. */
public String workloadHeader(String operation) {
if (operation == null || operation.isBlank()) {
throw new IllegalArgumentException("operation must be non-blank");
public String workloadHeader(Purpose purpose) {
return "java-chat:" + java.util.Objects.requireNonNull(purpose, "purpose").label + ":"
+ (this == LIVE ? "live" : "batch");
}

/** Identifies the application job independently of its wire protocol and capacity tier. */
public enum Purpose {
/** Answers a chat question. */
CHAT_ANSWER("chatAnswer"),
/** Generates a guided lesson. */
GUIDED_LEARNING("guidedLearning"),
/** Enriches rendered markdown. */
MARKDOWN_ENRICHMENT("markdownEnrichment"),
/** Ranks retrieved search candidates. */
SEARCH_RERANK("searchRerank"),
/** Embeds a live retrieval query. */
SEARCH_QUERY_EMBEDDING("searchQueryEmbedding"),
/** Embeds content supplied directly to the batch executor. */
CONTENT_INDEX_EMBEDDING("contentIndexEmbedding"),
Comment on lines +54 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the test-only content-index purpose

CONTENT_INDEX_EMBEDDING has no production use anywhere in the repository; every reference outside this declaration is in a test using it as a generic batch placeholder. Keeping it in the production purpose inventory creates a gateway workload label that no application job can emit, so tests should use a real production indexing purpose and this constant should be deleted.

AGENTS.md reference: AGENTS.md:L104-L104

Useful? React with 👍 / 👎.

/** Indexes repository source files. */
REPOSITORY_INDEX_EMBEDDING("repositoryIndexEmbedding"),
/** Indexes documentation. */
DOCUMENTATION_INDEX_EMBEDDING("documentationIndexEmbedding"),
/** Indexes book content. */
BOOK_INDEX_EMBEDDING("bookIndexEmbedding"),
/** Indexes articles. */
ARTICLE_INDEX_EMBEDDING("articleIndexEmbedding"),
/** Indexes PDF content. */
PDF_INDEX_EMBEDDING("pdfIndexEmbedding"),
/** Keeps the embedding model resident. */
EMBEDDING_WARMUP("embeddingWarmup");

private final String label;

Purpose(String label) {
this.label = label;
}
return "java-chat:" + operation + ":" + (this == LIVE ? "live" : "batch");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ public LocalEmbeddingClient(
}

@Override
public List<float[]> embed(List<String> texts, LlmGatewayTier requestTier) {
public List<float[]> embed(LlmGatewayTier.Purpose purpose, List<String> texts, LlmGatewayTier requestTier) {
Objects.requireNonNull(purpose, "purpose");
Objects.requireNonNull(requestTier, "requestTier");
if (texts == null || texts.isEmpty()) {
return List.of();
Expand All @@ -89,7 +90,9 @@ public List<float[]> embed(List<String> texts, LlmGatewayTier requestTier) {
* template whose connect and read timeouts cannot exceed the remaining budget.</p>
*/
@Override
public List<float[]> embed(List<String> texts, LlmGatewayTier requestTier, Duration requestTimeout) {
public List<float[]> embed(
LlmGatewayTier.Purpose purpose, List<String> texts, LlmGatewayTier requestTier, Duration requestTimeout) {
Objects.requireNonNull(purpose, "purpose");
Objects.requireNonNull(requestTier, "requestTier");
Duration requiredRequestTimeout = Objects.requireNonNull(requestTimeout, "requestTimeout");
if (texts == null || texts.isEmpty()) {
Expand Down
Loading
Loading