"""Text/Vector/Graph backend steps + Provenance + IndexResult steps.""" from __future__ import annotations from typing import Any from behave import then, when # type: ignore[import-untyped] from pydantic import ValidationError from cleveragents.domain.models.acms.analyzers import UKOTriple from cleveragents.domain.models.acms.index_backends import ( IndexedDocument, SearchResult, ) from cleveragents.domain.models.acms.provenance import ( IndexResult, ProvenancedTriple, ProvenanceMetadata, ) from features.steps.uko_indexer_common import ULID_1 __all__: list[str] = [] @when( 'idx I index a text document "{doc_id}" with content "{content}" in project "{project}"' ) def step_index_text_doc(context: Any, doc_id: str, content: str, project: str) -> None: context.idx_text_backend.index_document(project, doc_id, content, {}) @then('idx searching "{query}" in project "{project}" should return {count:d} result') def step_search_text_singular( context: Any, query: str, project: str, count: int ) -> None: results = context.idx_text_backend.search(project, query) assert len(results) == count, f"Expected {count} result, got {len(results)}" context.idx_search_results = results @then('idx searching "{query}" in project "{project}" should return {count:d} results') def step_search_text_plural(context: Any, query: str, project: str, count: int) -> None: results = context.idx_text_backend.search(project, query) assert len(results) == count, f"Expected {count} results, got {len(results)}" context.idx_search_results = results @then('idx the search result doc_id should be "{doc_id}"') def step_check_search_doc_id(context: Any, doc_id: str) -> None: assert context.idx_search_results[0].doc_id == doc_id @when('idx I remove text document "{doc_id}" from project "{project}"') def step_remove_text_doc(context: Any, doc_id: str, project: str) -> None: context.idx_text_backend.remove_document(project, doc_id) @when('idx I rebuild text index for project "{project}"') def step_rebuild_text_index(context: Any, project: str) -> None: context.idx_text_backend.rebuild_index(project) @then("idx indexing a text document with empty project should raise ValueError") def step_text_empty_project(context: Any) -> None: try: context.idx_text_backend.index_document("", "doc", "content", {}) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx indexing a text document with empty content should raise ValueError") def step_text_empty_content(context: Any) -> None: try: context.idx_text_backend.index_document("proj", "doc", "", {}) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx searching with empty query should raise ValueError") def step_text_empty_query(context: Any) -> None: try: context.idx_text_backend.search("proj", "") assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx searching with limit 0 should raise ValueError") def step_text_zero_limit(context: Any) -> None: try: context.idx_text_backend.search("proj", "q", limit=0) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @when( 'idx I index an embedding "{doc_id}" with vector "{vector_str}" in project "{project}"' ) def step_index_embedding( context: Any, doc_id: str, vector_str: str, project: str ) -> None: embedding = [float(v) for v in vector_str.split(",")] context.idx_vector_backend.index_embedding(project, doc_id, embedding, {}) @then('idx searching similar in project "{project}" should return {count:d} result') def step_search_similar_singular(context: Any, project: str, count: int) -> None: results = context.idx_vector_backend.search_similar(project, [1.0]) assert len(results) == count, f"Expected {count}, got {len(results)}" @then('idx searching similar in project "{project}" should return {count:d} results') def step_search_similar_plural(context: Any, project: str, count: int) -> None: results = context.idx_vector_backend.search_similar(project, [1.0]) assert len(results) == count, f"Expected {count}, got {len(results)}" @then( "idx searching similar in project" ' "{project}" with min_relevance {threshold} should return {count:d} results' ) def step_search_similar_min_relevance( context: Any, project: str, threshold: str, count: int, ) -> None: results = context.idx_vector_backend.search_similar( project, [1.0], min_relevance=float(threshold), ) assert len(results) == count, f"Expected {count}, got {len(results)}" @then("idx searching similar with min_relevance {threshold} should raise ValueError") def step_search_similar_invalid_min_relevance( context: Any, threshold: str, ) -> None: try: context.idx_vector_backend.search_similar( "local/app", [1.0], min_relevance=float(threshold), ) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @when('idx I remove embedding "{doc_id}" from project "{project}"') def step_remove_embedding(context: Any, doc_id: str, project: str) -> None: context.idx_vector_backend.remove_embedding(project, doc_id) @then("idx indexing an embedding with empty vector should raise ValueError") def step_vector_empty_embedding(context: Any) -> None: try: context.idx_vector_backend.index_embedding("proj", "doc", [], {}) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @when('idx I add a triple "{subj}" "{pred}" "{obj}" in project "{project}"') def step_add_triple(context: Any, subj: str, pred: str, obj: str, project: str) -> None: context.idx_graph_backend.add_triple(project, subj, pred, obj) @then('idx querying project "{project}" should return {count:d} binding') def step_query_graph_singular(context: Any, project: str, count: int) -> None: results = context.idx_graph_backend.query(project, "SELECT * WHERE { ?s ?p ?o }") assert len(results) == count, f"Expected {count}, got {len(results)}" context.idx_graph_results = results @then('idx querying project "{project}" should return {count:d} bindings') def step_query_graph_plural(context: Any, project: str, count: int) -> None: results = context.idx_graph_backend.query(project, "SELECT * WHERE { ?s ?p ?o }") assert len(results) == count, f"Expected {count}, got {len(results)}" context.idx_graph_results = results @then('idx the binding should have subject "{subject}"') def step_check_binding_subject(context: Any, subject: str) -> None: assert context.idx_graph_results[0]["s"] == subject @when('idx I remove triples with subject "{subject}" from project "{project}"') def step_remove_triples(context: Any, subject: str, project: str) -> None: context.idx_graph_backend.remove_triples( project, subject=subject, predicate=None, obj=None ) @then("idx removing triples with all-None pattern should raise ValueError") def step_graph_all_none(context: Any) -> None: try: context.idx_graph_backend.remove_triples("proj", None, None, None) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx adding a triple with empty project should raise ValueError") def step_graph_empty_project(context: Any) -> None: try: context.idx_graph_backend.add_triple("", "s", "p", "o") assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @when('idx I create provenance for resource "{resource_id}"') def step_create_provenance(context: Any, resource_id: str) -> None: context.idx_provenance = ProvenanceMetadata(source_resource=resource_id) @then('idx the provenance should have source_resource "{resource_id}"') def step_check_provenance_resource(context: Any, resource_id: str) -> None: assert context.idx_provenance.source_resource == resource_id @then("idx the provenance should be current") def step_check_provenance_current(context: Any) -> None: assert context.idx_provenance.is_current is True @then("idx the provenance should have a valid_from timestamp") def step_check_provenance_timestamp(context: Any) -> None: assert context.idx_provenance.valid_from is not None @then("idx creating provenance with empty source_resource should raise ValidationError") def step_provenance_empty_resource(context: Any) -> None: try: ProvenanceMetadata(source_resource="") assert False, "Expected ValidationError" # noqa: B011 except ValidationError: pass @when('idx I create a provenanced triple for resource "{resource_id}"') def step_create_provenanced_triple(context: Any, resource_id: str) -> None: triple = UKOTriple( subject_uri="uko://s", predicate="uko:type", object_uri="uko://C" ) provenance = ProvenanceMetadata(source_resource=resource_id) context.idx_pt = ProvenancedTriple(triple=triple, provenance=provenance) @then("idx the provenanced triple should have the triple") def step_check_pt_triple(context: Any) -> None: assert context.idx_pt.triple.subject_uri == "uko://s" @then("idx the provenanced triple should have the provenance") def step_check_pt_provenance(context: Any) -> None: assert context.idx_pt.provenance.source_resource == ULID_1 @then("idx mutating the provenance should raise ValidationError") def step_provenance_frozen(context: Any) -> None: try: context.idx_provenance.is_current = False # type: ignore[misc] assert False, "Expected error" # noqa: B011 except ValidationError: # Pydantic frozen models raise ValidationError on mutation pass # Verify the object wasn't actually mutated assert context.idx_provenance.is_current is True @when("idx I create an IndexResult with {triples:d} triples and {text:d} text doc") def step_create_index_result(context: Any, triples: int, text: int) -> None: context.idx_result = IndexResult( resource_id=ULID_1, triple_count=triples, text_docs_indexed=text, analyzer_domain="python", ) @then("idx the IndexResult should have triple_count {count:d}") def step_check_ir_triples(context: Any, count: int) -> None: assert context.idx_result.triple_count == count @then("idx the IndexResult should have text_docs_indexed {count:d}") def step_check_ir_text(context: Any, count: int) -> None: assert context.idx_result.text_docs_indexed == count @then('idx the IndexResult should have analyzer_domain "{domain}"') def step_check_ir_domain(context: Any, domain: str) -> None: assert context.idx_result.analyzer_domain == domain @then( "idx creating IndexResult with negative triple_count should raise ValidationError" ) def step_ir_negative(context: Any) -> None: try: IndexResult(resource_id=ULID_1, triple_count=-1) assert False, "Expected ValidationError" # noqa: B011 except ValidationError: pass @then("idx creating SearchResult with score 1.5 should raise ValueError") def step_search_result_bad_score(context: Any) -> None: try: SearchResult(doc_id="d", content="c", score=1.5) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx creating SearchResult with empty doc_id should raise ValueError") def step_search_result_empty_id(context: Any) -> None: try: SearchResult(doc_id="", content="c", score=0.5) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx creating IndexedDocument with negative char_count should raise ValueError") def step_indexed_doc_negative(context: Any) -> None: try: IndexedDocument(project="p", doc_id="d", char_count=-1) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx creating IndexedDocument with empty project should raise ValueError") def step_indexed_doc_empty_project(context: Any) -> None: try: IndexedDocument(project="", doc_id="d") assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then( "idx the graph backend should contain triple" ' with predicate "{predicate}" and object "{obj}"' ) def step_graph_contains_triple(context: Any, predicate: str, obj: str) -> None: backend = context.idx_graph_backend project = getattr(context, "idx_project", "local/test") bindings = backend.query(project, "SELECT *") matched = [b for b in bindings if b["p"] == predicate and b["o"] == obj] assert len(matched) > 0, ( f"No triple with predicate={predicate!r} object={obj!r} found in {bindings}" ) @then('idx the graph backend should not contain triple with object "{obj}"') def step_graph_not_contains_object(context: Any, obj: str) -> None: backend = context.idx_graph_backend project = getattr(context, "idx_project", "local/test") bindings = backend.query(project, "SELECT *") matched = [b for b in bindings if b["o"] == obj] assert len(matched) == 0, ( f"Found unexpected triple(s) with object={obj!r}: {matched}" ) @then('idx the provenance source_resource should be "{expected}"') def step_provenance_source_resource(context: Any, expected: str) -> None: pt = context.idx_pt assert pt.provenance.source_resource == expected, ( f"Expected source_resource={expected!r}, got {pt.provenance.source_resource!r}" ) @then('idx the provenance source_path should be "{expected}"') def step_provenance_source_path(context: Any, expected: str) -> None: pt = context.idx_pt assert pt.provenance.source_path == expected, ( f"Expected source_path={expected!r}, got {pt.provenance.source_path!r}" ) @then( "idx indexing a text document with whitespace-only project should raise ValueError" ) def step_text_whitespace_project(context: Any) -> None: try: context.idx_text_backend.index_document(" ", "doc", "content", {}) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then( "idx indexing a text document with whitespace-only content should raise ValueError" ) def step_text_whitespace_content(context: Any) -> None: try: context.idx_text_backend.index_document("proj", "doc", " ", {}) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx adding a triple with whitespace-only subject should raise ValueError") def step_graph_whitespace_subject(context: Any) -> None: try: context.idx_graph_backend.add_triple("proj", " ", "p", "o") assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then('idx the text backend should contain doc_id "{doc_id}" in project "{project}"') def step_text_contains_doc(context: Any, doc_id: str, project: str) -> None: key = (project, doc_id) assert key in context.idx_text_backend._docs, ( f"doc_id {doc_id!r} not found in text backend for project {project!r}" ) @then('idx the vector backend should contain doc_id "{doc_id}" in project "{project}"') def step_vector_contains_doc(context: Any, doc_id: str, project: str) -> None: key = (project, doc_id) assert key in context.idx_vector_backend._embeddings, ( f"doc_id {doc_id!r} not found in vector backend for project {project!r}" ) @then("idx creating IndexResult with empty resource_id should raise ValidationError") def step_ir_empty_resource_id(context: Any) -> None: try: IndexResult(resource_id="") assert False, "Expected ValidationError" # noqa: B011 except ValidationError: pass @then("idx creating SearchResult with score -0.1 should raise ValueError") def step_search_result_negative_score(context: Any) -> None: try: SearchResult(doc_id="d", content="c", score=-0.1) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx creating SearchResult with NaN score should raise ValueError") def step_search_result_nan_score(context: Any) -> None: try: SearchResult(doc_id="d", content="c", score=float("nan")) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx removing triples with empty-string subject should raise ValueError") def step_graph_empty_string_subject(context: Any) -> None: try: context.idx_graph_backend.remove_triples( "proj", subject="", predicate=None, obj=None ) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx removing triples with empty-string predicate should raise ValueError") def step_graph_empty_string_predicate(context: Any) -> None: try: context.idx_graph_backend.remove_triples( "proj", subject="s", predicate="", obj=None ) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass @then("idx removing triples with empty-string obj should raise ValueError") def step_graph_empty_string_obj(context: Any) -> None: try: context.idx_graph_backend.remove_triples( "proj", subject="s", predicate=None, obj="" ) assert False, "Expected ValueError" # noqa: B011 except ValueError: pass