From fbc7072a605b985ae88ffa4773882a98aa6a0dd4 Mon Sep 17 00:00:00 2001 From: jasper blues Date: Sat, 8 Aug 2026 07:04:02 +1000 Subject: [PATCH] Start when the host has no embedding model configured yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both graph-backend beans resolved the default embedding service while they were being created, via ai.withDefaultEmbeddingService(). That throws when no embedding model is registered, so a host whose provider key arrives at first run rather than at boot could not start — and could not reach the setup flow that would have supplied the key. drivinePropositionRepository now prefers the application's own EmbeddingService bean where there is an unambiguous one (a @Primary bean counts), falling back to the platform default. A host that supports late configuration registers a service that reports its own absence and can be switched on later; DrivinePropositionRepository only touches it when it actually embeds, so holding an absent-tolerant one is safe. propositionVectorIndexSchema registers nothing when there is no model, rather than guessing a dimension. An index built at the wrong dimension is worse than no index, because writes to it succeed and a real model later disagrees with everything already stored. The catalog is rebuilt on the next boot, by which time a model configured at first run is registered. Behaviour is unchanged for any deployment that has an embedding model. Co-Authored-By: Claude Opus 5 (1M context) --- .../DiceStorageAutoConfiguration.kt | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/dice-storage-autoconfigure/src/main/kotlin/com/embabel/dice/storage/autoconfigure/DiceStorageAutoConfiguration.kt b/dice-storage-autoconfigure/src/main/kotlin/com/embabel/dice/storage/autoconfigure/DiceStorageAutoConfiguration.kt index 51f4084f..e2fac013 100644 --- a/dice-storage-autoconfigure/src/main/kotlin/com/embabel/dice/storage/autoconfigure/DiceStorageAutoConfiguration.kt +++ b/dice-storage-autoconfigure/src/main/kotlin/com/embabel/dice/storage/autoconfigure/DiceStorageAutoConfiguration.kt @@ -16,6 +16,7 @@ package com.embabel.dice.storage.autoconfigure import com.embabel.agent.api.common.Ai +import com.embabel.common.ai.model.EmbeddingService import com.embabel.dice.spi.DecayStatusPolicy import com.embabel.dice.incremental.ChunkHistoryStore import com.embabel.dice.incremental.InMemoryChunkHistoryStore @@ -81,16 +82,32 @@ class DiceStorageAutoConfiguration { persistenceManager: PersistenceManager, ai: Ai, transactionManager: PlatformTransactionManager, + embeddingServices: ObjectProvider, ): PropositionRepository { logger.info( "Wiring graph proposition store (Drivine/Neo4j), vector index '{}'", DrivinePropositionRepository.VECTOR_INDEX, ) return DrivinePropositionRepository( - graphObjectManager, persistenceManager, ai.withDefaultEmbeddingService(), transactionManager, + graphObjectManager, persistenceManager, embeddingService(ai, embeddingServices), transactionManager, ) } + /** + * The application's own [EmbeddingService] bean where there is an unambiguous one + * (a `@Primary` bean counts), otherwise the platform default. + * + * Preferring the bean matters for a host that can start with NO embedding model + * configured — one whose provider key arrives at first run rather than at boot. + * Such a host registers an embedding service that reports its own absence and can be + * switched on later, whereas `ai.withDefaultEmbeddingService()` resolves the default + * eagerly and throws when no model is registered, taking the context down with it. + * [DrivinePropositionRepository] only touches the service when it actually embeds, so + * an absent-tolerant one is safe to hold. + */ + private fun embeddingService(ai: Ai, embeddingServices: ObjectProvider): EmbeddingService = + embeddingServices.getIfUnique() ?: ai.withDefaultEmbeddingService() + @Bean @ConditionalOnProperty(prefix = "embabel.dice.store", name = ["type"], havingValue = "graph") @ConditionalOnMissingBean(ChunkHistoryStore::class) @@ -163,9 +180,26 @@ class DiceStorageAutoConfiguration { havingValue = "true", matchIfMissing = true, ) - fun propositionVectorIndexSchema(ai: Ai): SchemaCatalog { - val embeddingService = ai.withDefaultEmbeddingService() - val spec = propositionVectorIndexSpec(embeddingService.dimensions) + fun propositionVectorIndexSchema( + ai: Ai, + embeddingServices: ObjectProvider, + ): SchemaCatalog { + // A vector index is created AT the embedding model's dimension, so with no model + // there is no dimension to create it at. Register nothing rather than guess: an + // index at the wrong dimension is worse than none, because writes to it succeed. + // The catalog is rebuilt on the next boot, by which time a model configured at + // first run is registered. + val embeddingService = runCatching { embeddingService(ai, embeddingServices) } + .getOrElse { + logger.warn("Skipping proposition vector index schema: no embedding model ({})", it.message) + return SchemaCatalog.of() + } + val dimensions = runCatching { embeddingService.dimensions } + .getOrElse { + logger.warn("Skipping proposition vector index schema: no embedding model ({})", it.message) + return SchemaCatalog.of() + } + val spec = propositionVectorIndexSpec(dimensions) logger.info("Registering proposition vector index schema: {} (model={})", spec, embeddingService.name) return SchemaCatalog.of(spec).withVersion(embeddingService.name) }