Skip to content
129 changes: 129 additions & 0 deletions integration/test_collection_decisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,135 @@ def test_decide_with_every_kind_of_question(collection_factory: CollectionFactor
assert list(objs[0].decisions) == ["angry"]


ANGRY = "the customer is angry about billing"
CALM = "all good, thanks"


def _texts(objs) -> list: # type: ignore[no-untyped-def]
return [o.properties["text"] for o in objs]


# A keyword search that returns both texts: rerank goes with a search, not
# with fetch_objects.
SEARCH = "customer good"


def test_rerank_by_a_decision(collection_factory: CollectionFactory) -> None:
collection = _collection(collection_factory)
collection.data.insert_many([{"text": ANGRY}, {"text": CALM}])

# The class settings are the defaults: search order, nothing dropped.
objs = collection.query.bm25(
SEARCH, rerank=wvc.query.Rerank.decide(prop="text", query="customer angry")
).objects
assert {o.properties["text"]: o.metadata.rerank_score for o in objs} == {ANGRY: 1.0, CALM: 0.0}

# A threshold drops the candidates below it.
objs = collection.query.bm25(
SEARCH,
rerank=wvc.query.Rerank.decide(prop="text", query="customer angry", min_probability=0.6),
).objects
assert _texts(objs) == [ANGRY]
assert objs[0].metadata.rerank_score == 1.0

# "all good, thanks" matches two of three words, the angry text one.
objs = collection.query.bm25(
SEARCH,
rerank=wvc.query.Rerank.decide(
prop="text",
query="good thanks customer",
min_probability=0.3,
order=wvc.query.DecisionsOrder.PROBABILITY,
),
).objects
assert _texts(objs) == [CALM, ANGRY]

# A plain rerank on a class with only a decisions module is a relevance predicate.
objs = collection.query.bm25(
SEARCH, rerank=wvc.query.Rerank(prop="text", query="customer angry")
).objects
assert {o.properties["text"]: o.metadata.rerank_score for o in objs} == {ANGRY: 1.0, CALM: 0.0}


def test_rerank_by_a_decision_takes_the_class_settings(
collection_factory: CollectionFactory,
) -> None:
collection = collection_factory(
decisions_config=wvc.config.Configure.Decisions.custom(
"decisions-dummy", {"minProbability": 0.9, "order": "probability"}
),
vectorizer_config=wvc.config.Configure.Vectorizer.none(),
properties=[wvc.config.Property(name="text", data_type=wvc.config.DataType.TEXT)],
)
if "decisions-dummy" not in collection._connection.get_meta()["modules"]:
pytest.skip("the server has no decisions-dummy module")
collection.data.insert_many([{"text": ANGRY}, {"text": CALM}])

objs = collection.query.bm25(
SEARCH, rerank=wvc.query.Rerank.decide(prop="text", query="good thanks customer")
).objects
assert objs == [], "no text has every word"

# The request overrides the threshold; the class order still applies.
objs = collection.query.bm25(
SEARCH,
rerank=wvc.query.Rerank.decide(
prop="text", query="good thanks customer", min_probability=0.3
),
).objects
assert _texts(objs) == [CALM, ANGRY]


def test_rerank_by_a_decision_without_a_decisions_module(
collection_factory: CollectionFactory,
) -> None:
collection = collection_factory(
reranker_config=wvc.config.Configure.Reranker.custom("reranker-dummy"),
vectorizer_config=wvc.config.Configure.Vectorizer.none(),
properties=[wvc.config.Property(name="text", data_type=wvc.config.DataType.TEXT)],
)
if "decisions-dummy" not in collection._connection.get_meta()["modules"]:
pytest.skip("the server has no decisions-dummy module")
collection.data.insert({"text": "x"})

# The server rejects it before the search.
with pytest.raises(WeaviateQueryError, match="no decisions module"):
collection.query.bm25("x", rerank=wvc.query.Rerank.decide(prop="text", query="x"))


def test_a_reranker_and_a_decisions_module_on_one_collection(
collection_factory: CollectionFactory,
) -> None:
collection = collection_factory(
reranker_config=wvc.config.Configure.Reranker.custom("reranker-dummy"),
decisions_config=wvc.config.Configure.Decisions.custom("decisions-dummy"),
vectorizer_config=wvc.config.Configure.Vectorizer.none(),
properties=[wvc.config.Property(name="text", data_type=wvc.config.DataType.TEXT)],
)
if "decisions-dummy" not in collection._connection.get_meta()["modules"]:
pytest.skip("the server has no decisions-dummy module")
collection.data.insert_many([{"text": ANGRY}, {"text": CALM}])

# A plain rerank goes to the reranker, which scores by length; decide goes to the
# decisions module.
objs = collection.query.bm25(
"billing good",
rerank=wvc.query.Rerank(prop="text", query="customer angry"),
decide=wvc.query.Decision.predicate("angry", prop="text", instructions="customer angry"),
).objects
assert _texts(objs) == [ANGRY, CALM]
assert [o.metadata.rerank_score for o in objs] == [float(len(ANGRY)), float(len(CALM))]
assert [o.decisions["angry"].probability for o in objs if o.decisions] == [1.0, 0.0]

# A decision goes to the decisions module: the probability is the score.
objs = collection.query.bm25(
"billing good",
rerank=wvc.query.Rerank.decide(prop="text", query="customer angry", min_probability=0.6),
).objects
assert _texts(objs) == [ANGRY]
assert objs[0].metadata.rerank_score == 1.0


def test_decide_is_rejected_with_group_by(collection_factory: CollectionFactory) -> None:
collection = _collection(collection_factory)
collection.data.insert({"text": "x"})
Expand Down
7 changes: 2 additions & 5 deletions integration/test_collection_decisions_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

import weaviate.classes as wvc
from weaviate.collections.classes.config import Decisions, DecisionsQuestion
from weaviate.collections.classes.config import Decisions

from .conftest import CollectionFactory

Expand All @@ -14,9 +14,7 @@ def test_decisions_openai(collection_factory: CollectionFactory) -> None:
pytest.skip("No OpenAI API key found.")

collection = collection_factory(
decisions_config=wvc.config.Configure.Decisions.openai(
question=DecisionsQuestion.STATEMENT, max_documents=10
),
decisions_config=wvc.config.Configure.Decisions.openai(max_documents=10),
vectorizer_config=wvc.config.Configure.Vectorizer.none(),
properties=[wvc.config.Property(name="text", data_type=wvc.config.DataType.TEXT)],
headers={"X-OpenAI-Api-Key": api_key},
Expand All @@ -27,7 +25,6 @@ def test_decisions_openai(collection_factory: CollectionFactory) -> None:
config = collection.config.get()
assert config.decisions_config is not None
assert config.decisions_config.decisions == Decisions.OPENAI
assert config.decisions_config.model["question"] == "statement"
assert config.decisions_config.model["maxDocuments"] == 10

collection.data.insert_many(
Expand Down
63 changes: 62 additions & 1 deletion mock_tests/test_decisions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import pytest
from pydantic import ValidationError

import weaviate
from mock_tests.conftest import MockDecisionsWeaviateService
from weaviate.classes.query import Decision, GroupBy
from weaviate.classes.query import Decision, GroupBy, Rerank, DecisionsOrder
from weaviate.collections.classes.internal import DecisionAnswer
from weaviate.exceptions import WeaviateInvalidInputError
from weaviate.proto.v1 import search_get_pb2
Expand Down Expand Up @@ -122,3 +123,63 @@ def test_decide_is_rejected_before_the_request(
decide=QUESTIONS[0],
)
assert service.captured_request.collection == "", "nothing was sent"


def test_rerank_by_a_decision_is_sent(
decisions_collection: tuple[weaviate.collections.Collection, MockDecisionsWeaviateService],
) -> None:
collection, service = decisions_collection

collection.query.bm25(
"billing",
rerank=Rerank.decide(
prop="text", query="angry", min_probability=0.6, order=DecisionsOrder.PROBABILITY
),
)
sent = service.captured_request.rerank
assert sent.property == "text"
assert sent.query == "angry"
assert sent.decision.min_probability == 0.6
assert sent.decision.order == search_get_pb2.RerankOrder.RERANK_ORDER_PROBABILITY

# Unset options are left unset, so the class settings apply.
collection.query.bm25("billing", rerank=Rerank.decide(prop="text", query="angry"))
sent = service.captured_request.rerank
assert sent.HasField("decision")
assert not sent.decision.HasField("min_probability")
assert not sent.decision.HasField("order")

collection.query.bm25("billing", rerank=Rerank(prop="text", query="angry"))
assert not service.captured_request.rerank.HasField("decision")


@pytest.mark.parametrize("min_probability", [-0.1, 1.1])
def test_rerank_by_a_decision_needs_a_probability(min_probability: float) -> None:
with pytest.raises(ValidationError):
Rerank.decide(prop="text", query="angry", min_probability=min_probability)


@pytest.mark.parametrize("query", [None, "", " "])
def test_rerank_by_a_decision_needs_a_query(query) -> None:
with pytest.raises(WeaviateInvalidInputError, match="Rerank.decide needs the query"):
Rerank.decide(prop="text", query=query)


@pytest.mark.parametrize("order", ["PROBABILITY", "random", 2])
def test_rerank_by_a_decision_names_a_bad_order(order) -> None:
with pytest.raises(WeaviateInvalidInputError, match="order must be one of"):
Rerank.decide(prop="text", query="angry", order=order)


def test_rerank_by_a_decision_takes_the_order_as_a_string(
decisions_collection: tuple[weaviate.collections.Collection, MockDecisionsWeaviateService],
) -> None:
collection, service = decisions_collection
order = "probability"

collection.query.bm25("billing", rerank=Rerank.decide(prop="text", query="angry", order=order))

assert (
service.captured_request.rerank.decision.order
== search_get_pb2.RerankOrder.RERANK_ORDER_PROBABILITY
)
7 changes: 2 additions & 5 deletions test/collection/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
_CollectionConfigUpdate,
DecisionsOrder,
DecisionsProvider,
DecisionsQuestion,
_GenerativeProvider,
_ReplicationConfig,
_ReplicationConfigUpdate,
Expand Down Expand Up @@ -1444,7 +1443,6 @@ def test_config_with_generative(
model="gpt-6-luna",
base_url="https://some.base.url/",
max_documents=50,
question=DecisionsQuestion.STATEMENT,
min_probability=0.6,
order=DecisionsOrder.SEARCH,
),
Expand All @@ -1453,7 +1451,6 @@ def test_config_with_generative(
"model": "gpt-6-luna",
"baseURL": "https://some.base.url/",
"maxDocuments": 50,
"question": "statement",
"minProbability": 0.6,
"order": "search",
},
Expand Down Expand Up @@ -1533,9 +1530,9 @@ def test_config_with_a_reranker_and_a_decisions_module() -> None:
(
{"decisions-typesafeai": {"minProbability": 0.5}},
_CollectionConfigUpdate(
decisions_config=Reconfigure.Decisions.openai(question=DecisionsQuestion.STATEMENT)
decisions_config=Reconfigure.Decisions.openai(max_documents=20)
),
{"decisions-openai": {"question": "statement"}},
{"decisions-openai": {"maxDocuments": 20}},
),
(
{"text2vec-contextionary": {}},
Expand Down
2 changes: 0 additions & 2 deletions weaviate/classes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
DecisionsConfig,
DecisionsOrder,
DecisionsProvider,
DecisionsQuestion,
GenerativeProvider,
GenerativeSearches,
IndexName,
Expand Down Expand Up @@ -93,7 +92,6 @@
"DecisionsConfig",
"DecisionsOrder",
"DecisionsProvider",
"DecisionsQuestion",
"RerankerProvider",
"Rerankers",
"ShardingConfigCreate",
Expand Down
6 changes: 4 additions & 2 deletions weaviate/classes/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from weaviate.collections.classes.grpc import (
Boost,
BoostReturn,
DecideQuestion,
Decision,
DecisionQuestion,
DecisionsOrder,
Diversity,
GroupBy,
HybridFusion,
Expand Down Expand Up @@ -45,7 +46,8 @@
"Boost",
"BoostReturn",
"Decision",
"DecideQuestion",
"DecisionQuestion",
"DecisionsOrder",
"Rerank",
"Sort",
"TargetVectors",
Expand Down
Loading
Loading