Migrate embedding generation to omop-llm, plus oa-configurator 1.0 - #53
Draft
nicoloesch wants to merge 10 commits into
Draft
Migrate embedding generation to omop-llm, plus oa-configurator 1.0#53nicoloesch wants to merge 10 commits into
nicoloesch wants to merge 10 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR is predominantly the omop-llm rewrite: replacing omop-emb's own hand-rolled embedding-model client with
omop_llm.ModelBackend, the same shared contract the rest of the stack is converging on. Theoa-configurator1.0RefTomigration is a smaller piece riding along at the end, since it's what makes the newembedding_model_namefield resolvable in the first place.omop-llm rewrite (the bulk of this PR)
embeddings/embedding_client.py(EmbeddingClient, its ownEmbeddingRoleenum, batching, dimension auto-discovery, cosine-similarity helpers)embeddings/embedding_providers.py(EmbeddingProvider/OllamaProvider/OpenAIProvider, the package's own provider abstraction)embeddings/__init__.pyomop_llm.ModelBackenddoes the actual model calling now.omop_llm.EmbeddingRole(re-exported fromomop_emb.__init__) replaces the local enum.EmbeddingWriterInterfacenow builds and owns aModelBackenddirectly at construction time viabuild_model_backend_from_resolved(resolved_model), whereresolved_modelis anoa_configurator.ResolvedModel(fromResolver.resolve_model(cfg.embedding_model_name)):embedding_dim, anddocument_prefix/query_prefixall come from that resolved model, not fromomop-emb's own config fields.document_embedding_prefix/query_embedding_prefixconfig fields intoModelBackend.embed_texts(role=...):omop_emb.interface.EmbeddingReaderInterface.generate_embeddings(new: a static method, callable without an interface instance) just forwardsroleand validates the returned shape. It doesn't apply the prefix itself anymore.EmbeddingWriterInterface.embedding_dimnow resolves lazily viaModelBackend.dimensions()'s three-tier lookup (configured override, then provider fast path, then live probe) instead of the oldembedding_dimconfig-field hint plusEmbeddingClient's own discovery logic.cli_embeddings.py):add_embeddings/create_index's--api-base/--api-key/--provider/--modelflags are gone, replaced by a single--model-name/-mflag naming a[models.*]entry (defaults tocfg.embedding_model_name), resolved via a new_resolve_model()helper that turns an unknown model/provider name into an actionableomop-config models add .../providers add ...message instead of a rawKeyError.cli/cli_legacy.pyentirely (285 lines). The whole "legacy commands for backward compatibility" subcommand group existed only to import pre-built embeddings using the oldProviderType-based flow;cli_app.pyno longer registers it as a subcommand.test_embedding_client.py(649 lines),test_ollama_provider_api.py(124),test_providers.py(133)test_embedding_generation.py(219 lines), covering theomop_llm-backed path insteadomop_llm-based flow instead of the old client:docs/usage/interface-guide.md,docs/usage/asymmetric-embeddings.md,docs/usage/cli.md,docs/usage/configuration.md,docs/usage/installation.md,docs/usage/backend-selection.md,docs/index.md,README.md.oa-configurator 1.0 migration (smaller, rides along)
OmopEmbConfigoffResourceSpec/ModelFieldSpec/ResourceRef/owned_resources/required_resources/test_resources/referenced_modelsontoRefTo-marked fields:cdm_db(required)emb_db(deliberately optional,str | Nonedefaulting toNone, since it's a pgvector-only requirement; sqlitevec doesn't need a database entry)test_emb_db(optional,RefTo(DatabaseConfig, is_test=True))embedding_model_name(RefTo(ModelConfig), unchanged field name, now properly typed and what the omop-llm rewrite above resolves through)TEST_DBClassVaris removed entirelyfrom omop_alchemy.config import OmopAlchemyConfigimport. Cross-package database sharing is naming-convention only under the new model, no typed import needed.resolve_omop_cdm_engine()/resolve_omop_emb_engine()now resolve viaOmopEmbConfig.get_config().cdm_db/.emb_dbinstead ofOmopAlchemyConfig.CDM_DB.semantic_name/OmopEmbConfig.EMB_DB.semantic_name.emb_dbis optional,resolve_omop_emb_engine()checksbackendfirst (raising a clearRuntimeErrorif it's ever called outside the pgvector backend) and then raises ifemb_dbis unset, rather than passingNonethrough toget_engine().tests/conftest.py:resolve_test_resource(OmopEmbConfig.TEST_DB)→resolve_test_database(OmopEmbConfig, "test_emb_db")@pytest.mark.requires_resource→@pytest.mark.requires_database("test_emb_db")(5 call sites acrosstest_cli_pgvector_snapshot.py,test_pgvector_index_manager.py,test_pgvector.py). Same plain-string trade-off asorm-loader's marker fix; seeoa-configurator's own PR, "Test-database wiring," for the full reasoning.OmopEmbConfigimport was dropped from all three of those files.pyproject.toml:oa-configuratorpins reworded to>=1.0.0,<2.0.0; the already-commented-outomop-llmdependency line's syntax fixed ("omop-llm">=0.2,was invalid TOML if ever uncommented) and its TODO comment clarified to name the eventual target range;openaidropped.Relationship to open PRs and issues
Config resolution boundary
Absorbs [v2.0] Stop reading OmopEmbConfig deep inside EmbeddingClient and resolve_backend #50 rather than merged standalone. Its desing is adopted here, extended to use the new
vector_storesdomain instead ofOmopEmbConfigfields directly.OmopEmbConfigdrops its flatbackend/sqlite_path/emb_db/faiss_cache_dirfields entirely, replaced by a singlevector_store_name: Annotated[str, RefTo(VectorStoreConfig)] = "vector_store".resolve_backend()inbackends/base_backend.pybecomes a pure function:resolve_backend(backend_type: str | BackendType, *, database: ResolvedDatabase) -> EmbeddingBackend.sqlite_pathstring. Every backend, including an in-memory sqlite-vec store, is backed by a realdatabase: dialect and (for sqlitevec) the filesystem path are both read offdatabase.connection.urlviasqlalchemy.engine.make_url, not passed as a separate parameter. A sqlitevec store pointed at a non-sqlite connection, or a pgvector store pointed at a non-postgres one, now fails loudly with the actual dialect named, instead of silently misbehaving.New
resolve_backend_from_resolved(resolved: ResolvedVectorStore) -> EmbeddingBackendis the one construction entry point for a resolved vector store, mirroringomop_llm.build_model_backend_from_resolved(resolved: ResolvedModel)exactly.SQLiteVecEmbeddingBackend/PGVectorEmbeddingBackendas before.All CLI call sites updated:
Resolver.from_active_config().resolve_vector_store(cfg.vector_store_name)→resolve_backend_from_resolved(resolved).resolve_omop_emb_engine()(the pgvector database engine, previously keyed offcfg.emb_db) now resolves throughresolved.databaseon the sameResolvedVectorStore.resolve_omop_emb_engine()itself was later deleted outright:git log -Sconfirmed its one historical caller was removed whenresolve_backend()became a pure function, and it had zero callers left anywhere in the codebase.search()'s (cli_embeddings.py)--faiss-cache-dirfallback readsresolved_vector_store.faiss_cache_dirdirectly, now that oa-configurator'sVectorStoreConfig/ResolvedVectorStorecarry it as a dedicated field rather than aconfiguration["faiss_cache_dir"]dict key.Concept-similarity queries
Absorbs Add joint/centroid embedding and concept-to-concept similarity queries #40 rather than merged standalone: its branch predates this session's
interface.pyrework and no longer merges cleanly. The feature itself is re-ported by hand onto the current interface, not applied as a patch.EmbeddingReaderInterface(soEmbeddingWriterInterfaceinherits it too) gains two new methods, both read-only over already-stored embeddings, noModelBackend/generation involved:get_joint_embedding(concept_ids, weights=None) -> np.ndarray: the centroid (unweighted or weighted mean) of several concepts' stored embeddings, e.g. for querying by a combination of conditions. Explicitly not L2-normalized even underMetricType.COSINE, since backends compute cosine distance directly off raw stored vectors, not off this method's output. Raises on emptyconcept_ids, mismatchedweightslength, orweightssumming to zero (a real bug in the original branch, fixed there and preserved here, with a new dedicated test since the original never actually had one).get_similar_concepts(concept_ids, k=None, *, concept_filter=None, faiss_index_config=None) -> Tuple[Tuple[NearestConceptMatch, ...], ...]: neighbours of concepts that are already embedded, without the caller fetching and passing a raw vector themselves. Accepts a bare int or a sequence; one result row per input, self-excluded, in input order. Over-fetches by one neighbour internally sokreal results remain even when the self-match isn't guaranteed present.EmbeddingConceptFilteralready matches the reverted, single-class design, andget_similar_conceptsresolves its effectivekthe same simple wayget_nearest_conceptsalready does -> Deprecation behaviour outlined in Add joint/centroid embedding and concept-to-concept similarity queries #40 is removed.Docstring note that
EmbeddingConceptFilter's field shape duplicatesomop_graph.graph.constraints.SearchConstraintConcept(sinceomop_embcan't importomop_graph), tracked inOMOP_Alchemy#11, folded intoEmbeddingConceptFilter's existing docstring.docs/usage/interface-guide.md: two new subsections, "Query similar concepts" and "Combine embeddings (joint/centroid queries)", ported from the branch. Note thatget_joint_embeddingreturns a 1-D(D,)vector butget_nearest_conceptsexpects(Q, D), so callers reshape it themselves (joint_vec[None, :])Loosen versioning constraints