Repository navigation
feat: allow setting query transformers in BaseRAGQuestionAnswerer #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
96a13ee
a079385
b67afef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,21 @@ | |
| from .utils import build_vector_store, create_rag_app | ||
|
|
||
|
|
||
| class _QueryRewriteMockChat(llms.BaseChat): | ||
| """Rewrites the rewrite-prompt to a fixed search query; passes other prompts through.""" | ||
|
|
||
| def _accepts_call_arg(self, arg_name: str) -> bool: | ||
| return False | ||
|
|
||
| async def __wrapped__( | ||
| self, messages: list[dict] | pw.Json, model: str | ||
| ) -> str | None: | ||
| content = messages[0]["content"].as_str() | ||
| if content.startswith("rewrite:"): | ||
| return content.removeprefix("rewrite:") or None | ||
| return model + "," + content | ||
|
|
||
|
|
||
| @pw.udf | ||
| def fake_embeddings_model(x: str) -> list[float]: | ||
| return [ | ||
|
|
@@ -99,6 +114,156 @@ def test_base_rag(): | |
| ) | ||
|
|
||
|
|
||
| def test_rag_app_set_query_transformer_prompt(): | ||
| def query_transformer_prompt(query: str) -> str: | ||
| return f"rewrite:{query}" | ||
|
|
||
| rag_app = create_rag_app(query_transformer_prompt=query_transformer_prompt) | ||
|
|
||
| assert isinstance(rag_app.query_transformer_prompt_udf, pw.UDF) | ||
|
|
||
| assert _unwrap_udf(rag_app.query_transformer_prompt_udf)("foo") == "rewrite:foo" | ||
|
|
||
|
|
||
| def test_rag_app_no_query_transformer_by_default(): | ||
| rag_app = create_rag_app() | ||
|
|
||
| assert rag_app.query_transformer_prompt_udf is None | ||
|
|
||
|
|
||
| def test_base_rag_uses_original_prompt_for_answer_without_transformer(): | ||
| schema = pw.schema_from_types(data=bytes, _metadata=dict) | ||
| input = pw.debug.table_from_rows( | ||
| schema=schema, rows=[("foo", {}), ("bar", {}), ("baz", {})] | ||
| ) | ||
|
|
||
| vector_server = VectorStoreServer( | ||
| input, | ||
| embedder=fake_embeddings_model, | ||
| ) | ||
|
|
||
| rag = BaseRAGQuestionAnswerer( | ||
| IdentityMockChat(), | ||
| vector_server, | ||
| prompt_template=_prompt_template, | ||
| summarize_template=_summarize_template, | ||
| search_topk=1, | ||
| ) | ||
|
|
||
| answer_queries = pw.debug.table_from_rows( | ||
| schema=rag.AnswerQuerySchema, | ||
| rows=[ | ||
| ("foo", None, "gpt3.5", False), | ||
| ], | ||
| ) | ||
|
|
||
| answer_output = rag.answer_query(answer_queries) | ||
|
|
||
| casted_table = answer_output.select( | ||
| result=pw.apply_with_type(lambda x: x.value, str, pw.this.result["response"]) | ||
| ) | ||
|
|
||
| assert_table_equality( | ||
| casted_table, | ||
| pw.debug.table_from_markdown( | ||
| """ | ||
| result | ||
| gpt3.5,foo | ||
| """ | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("rewritten_query", ["bar", None]) | ||
| def test_base_rag_query_transformer_used_only_for_retrieval( | ||
| rewritten_query: str | None, | ||
| ): | ||
| @pw.udf | ||
| def query_transformer_prompt(query: str) -> str: | ||
| return f"rewrite:{rewritten_query or ''}" | ||
|
|
||
| schema = pw.schema_from_types(data=bytes, _metadata=dict) | ||
| input = pw.debug.table_from_rows( | ||
| schema=schema, rows=[("foo", {}), ("bar", {}), ("baz", {})] | ||
| ) | ||
|
|
||
| vector_server = VectorStoreServer( | ||
| input, | ||
| embedder=fake_embeddings_model, | ||
| ) | ||
|
|
||
| rag = BaseRAGQuestionAnswerer( | ||
| _QueryRewriteMockChat(), | ||
| vector_server, | ||
| prompt_template=_prompt_template, | ||
| query_transformer_prompt=query_transformer_prompt, | ||
| summarize_template=_summarize_template, | ||
| search_topk=1, | ||
| ) | ||
|
|
||
| answer_queries = pw.debug.table_from_rows( | ||
| schema=rag.AnswerQuerySchema, | ||
| rows=[ | ||
| ("foo", None, "gpt3.5", False), | ||
| ], | ||
| ) | ||
|
|
||
| answer_output = rag.answer_query(answer_queries) | ||
|
|
||
| casted_table = answer_output.select( | ||
| search_query=pw.this.search_query, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we also assert which document was retrieved here? The test checks the I verified this by changing With the existing documents and embeddings, please assert that:
For example, include |
||
| retrieved_text=pw.unwrap(pw.this.docs[0]["text"].as_str()), | ||
| result=pw.apply_with_type(lambda x: x.value, str, pw.this.result["response"]), | ||
| ) | ||
|
|
||
| # Retrieval uses the rewritten query, falling back to the original prompt | ||
| # when the LLM returns None. The final LLM call still uses the original prompt. | ||
| assert_table_equality( | ||
| casted_table, | ||
| pw.debug.table_from_markdown( | ||
| f""" | ||
| search_query | retrieved_text | result | ||
| {rewritten_query or 'foo'} | {rewritten_query or 'foo'} | gpt3.5,foo | ||
| """ | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def test_base_rag_fully_async_query_transformer_prompt(): | ||
| @pw.udf(executor=pw.udfs.fully_async_executor()) | ||
| async def query_transformer_prompt(query: str) -> str: | ||
| return f"rewrite:{query}" | ||
|
|
||
| rag = BaseRAGQuestionAnswerer( | ||
| _QueryRewriteMockChat(), | ||
| build_vector_store(fake_embeddings_model), | ||
| prompt_template=_prompt_template, | ||
| query_transformer_prompt=query_transformer_prompt, | ||
| search_topk=1, | ||
| ) | ||
| answer_queries = pw.debug.table_from_rows( | ||
| schema=rag.AnswerQuerySchema, | ||
| rows=[("foo", None, "gpt3.5", False)], | ||
| ) | ||
|
|
||
| answer_output = rag.answer_query(answer_queries) | ||
|
|
||
| assert_table_equality( | ||
| answer_output.select( | ||
| search_query=pw.this.search_query, | ||
| result=pw.apply_with_type( | ||
| lambda x: x.value, str, pw.this.result["response"] | ||
| ), | ||
| ), | ||
| pw.debug.table_from_markdown( | ||
| """ | ||
| search_query | result | ||
| foo | gpt3.5,foo | ||
| """ | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def test_rag_app_set_prompt(): | ||
| prompt_template = "Answer the question. Context: {context}\nQuestion: {query}" | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing the nullable LLM result! There is another case that fails while building the graph: a
query_transformer_promptUDF configured withfully_async_executor().The prompt UDF produces
Future[str], but this call toprompt_chat_single_qaconsumes it before the existingawait_futures()call. Here is a standalone reproduction, using a mock LLM and local documents (no API keys or external requests):This raises:
Switching only the prompt executor to
pw.udfs.async_executor()makes the graph build successfully.Could you add
pw_ai_queries = pw_ai_queries.await_futures()immediately after computing thequery_transformer_promptcolumn, before constructing the chat message? The existing wait after the LLM call is still needed. I verified locally that this additional wait fixes the reproduction.Please also add a regression test using a fully asynchronous prompt UDF. The constructor accepts
pw.UDFwithout an executor restriction, so this configuration should work too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for both finds, that's a real gap.
On the test assertions: added checks on the retrieved document itself. With rewrite bar, the retrieved doc now matches bar's expected content while the answer stays built from the original foo and with a None rewrite, it matches foo's. I confirmed the old tests would've passed even with the feature disabled, and reverting either fix now fails its corresponding test.
On the async UDF bug: reproduced your script exactly, it failed with the same TypeError. Added await_futures() right after the query_transformer_prompt column is computed, keeping the existing wait after the LLM rewrite. Added a regression test using a fully async prompt UDF, following your repro's structure.