From 1a5d1130f45372cd00d2f8d006937aab59388254 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sun, 12 Mar 2023 18:19:54 -0600 Subject: [PATCH 01/12] Generate search queries from message to answer users chat questions The Search Actor allows for 1. Looking up multiple pieces of information from the notes E.g "Is Bob older than Tom?" searches for age of Bob and Tom in 2 searches 2. Allow date aware user queries in Khoj chat Answer time range based questions Limit search to specified timeframe in question using date filter E.g "What national parks did I visit last year?" adds dt>="2022-01-01" dt<"2023-01-01" to Khoj search Note: Temperature set to 0. Message to search queries should be deterministic --- src/khoj/processor/conversation/gpt.py | 44 ++++++++++++++++++++++++++ src/khoj/routers/api.py | 14 +++++--- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index 986ffc17..df6d851a 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -78,6 +78,50 @@ Summarize the notes in second person perspective:""" return str(story).replace("\n\n", "") +def extract_questions(text, model="text-davinci-003", api_key=None, temperature=0, max_tokens=100): + """ + Infer search queries to retrieve relevant notes to answer user query + """ + # Initialize Variables + openai.api_key = api_key or os.getenv("OPENAI_API_KEY") + + # Get dates relative to today for prompt creation + today = datetime.today() + current_new_year = today.replace(month=1, day=1) + last_new_year = current_new_year.replace(year=today.year - 1) + + prompt = f""" +You are Khoj, a chat assistant with the ability to search the users notes +What searches, if any, will you need to perform to answer the users question below? Provide search queries as a JSON list of strings +Current Date: {today.strftime("%HH:%MM %A, %Y-%m-%d")} + +Q: How was my trip to Cambodia? + +["My Cambodia trip experience"] + +Q: How are you feeling? + +[] + +Q: What national parks did I go to last year? + +["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""] + +Q: Is Bob older than Tom? + +["When was Bob born?", "What is Tom's age?"] + +Q: {text}""" + + # Get Response from GPT + response = openai.Completion.create(prompt=prompt, model=model, temperature=temperature, max_tokens=max_tokens) + + # Extract, Clean Message from GPT's Response + questions = json.loads(response["choices"][0]["text"].strip(empty_escape_sequences)) + logger.debug(f"Extracted Questions by GPT: {questions}") + return questions + + def extract_search_type(text, model, api_key=None, temperature=0.5, max_tokens=100, verbose=0): """ Extract search type from user query using OpenAI's GPT diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 4839df48..2bbb0edf 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -10,7 +10,7 @@ from fastapi import HTTPException # Internal Packages from khoj.configure import configure_processor, configure_search -from khoj.processor.conversation.gpt import converse +from khoj.processor.conversation.gpt import converse, extract_questions from khoj.processor.conversation.utils import message_to_log, message_to_prompt from khoj.search_type import image_search, text_search from khoj.utils.helpers import timer @@ -191,6 +191,7 @@ def update(t: Optional[SearchType] = None, force: Optional[bool] = False): def chat(q: Optional[str] = None): # Initialize Variables api_key = state.processor_config.conversation.openai_api_key + model = state.processor_config.conversation.model # Load Conversation History chat_session = state.processor_config.conversation.chat_session @@ -203,9 +204,14 @@ def chat(q: Optional[str] = None): else: return {"status": "ok", "response": []} - # Collate context for GPT - result_list = search(q, n=2, r=True, score_threshold=0, dedupe=False) - collated_result = "\n\n".join([f"# {item.additional['compiled']}" for item in result_list]) + # Extract search queries from user message + queries = extract_questions(q, model=model, api_key=api_key) + + # Collate search results as context for GPT + result_list = [] + for query in queries: + result_list.extend(search(query, n=2, r=True, score_threshold=0, dedupe=False)) + collated_result = "\n\n".join({f"# {item.additional['compiled']}" for item in result_list}) logger.debug(f"Reference Context:\n{collated_result}") try: From d871e04a81d1026b6cf2be864fde414e1a03bf48 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 16 Mar 2023 15:14:47 -0600 Subject: [PATCH 02/12] Use past user messages, inferred questions as context to extract questions - Keep inferred questions in logs - Improve prompt to GPT to try use past questions as context - Pass past user message and inferred questions as context to help GPT extract complete questions - This should improve search results quality - Example Expected Inferred Questions from User Message using History: 1. "What is the name of Arun's daughter?" => "What is the name of Arun's daughter" 2. "Where does she study?" => => "Where does Arun's daughter study?" OR => "Where does Arun's daughter, Reena study?" --- src/khoj/processor/conversation/gpt.py | 37 +++++++++++++++++++++----- src/khoj/routers/api.py | 12 +++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index df6d851a..f9c81854 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -78,40 +78,63 @@ Summarize the notes in second person perspective:""" return str(story).replace("\n\n", "") -def extract_questions(text, model="text-davinci-003", api_key=None, temperature=0, max_tokens=100): +def extract_questions( + text, model="text-davinci-003", conversation_log={}, api_key=None, temperature=0, max_tokens=100 +): """ Infer search queries to retrieve relevant notes to answer user query """ # Initialize Variables openai.api_key = api_key or os.getenv("OPENAI_API_KEY") + # Extract Past User Message and Inferred Questions from Conversation Log + chat_history = "".join( + [ + f'Q: {chat["intent"]["query"]}\n\n{chat["intent"].get("inferred-queries") or list([chat["intent"]["query"]])}\n\n' + for chat in conversation_log.get("chat", [])[-4:] + if chat["by"] == "khoj" + ] + ) + # Get dates relative to today for prompt creation today = datetime.today() current_new_year = today.replace(month=1, day=1) last_new_year = current_new_year.replace(year=today.year - 1) prompt = f""" -You are Khoj, a chat assistant with the ability to search the users notes -What searches, if any, will you need to perform to answer the users question below? Provide search queries as a JSON list of strings +You are Khoj, a chat assistant with the ability to search the users notes and continue the existing conversation. +What searches, if any, will you need to perform to answer the users question below? +Provide search queries as a JSON list of strings Current Date: {today.strftime("%HH:%MM %A, %Y-%m-%d")} Q: How was my trip to Cambodia? -["My Cambodia trip experience"] +["How was my trip to Cambodia?"] -Q: How are you feeling? +Q: When did i go there? -[] +["When did I go to Cambodia?"] Q: What national parks did I go to last year? ["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""] +Q: How are you feeling today? + +[] + Q: Is Bob older than Tom? ["When was Bob born?", "What is Tom's age?"] -Q: {text}""" +Q: What is their age difference? + +["What is Bob's age?", "What is Tom's age?"] + +{chat_history} +Q: {text} + +""" # Get Response from GPT response = openai.Completion.create(prompt=prompt, model=model, temperature=temperature, max_tokens=max_tokens) diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 2bbb0edf..7169bbac 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -204,15 +204,14 @@ def chat(q: Optional[str] = None): else: return {"status": "ok", "response": []} - # Extract search queries from user message - queries = extract_questions(q, model=model, api_key=api_key) + # Infer search queries from user message + inferred_queries = extract_questions(q, model=model, api_key=api_key, conversation_log=meta_log) # Collate search results as context for GPT result_list = [] - for query in queries: + for query in inferred_queries: result_list.extend(search(query, n=2, r=True, score_threshold=0, dedupe=False)) collated_result = "\n\n".join({f"# {item.additional['compiled']}" for item in result_list}) - logger.debug(f"Reference Context:\n{collated_result}") try: gpt_response = converse(collated_result, q, meta_log, api_key=api_key) @@ -224,7 +223,10 @@ def chat(q: Optional[str] = None): # Update Conversation History state.processor_config.conversation.chat_session = message_to_prompt(q, chat_session, gpt_message=gpt_response) state.processor_config.conversation.meta_log["chat"] = message_to_log( - q, gpt_response, khoj_message_metadata={"context": collated_result}, conversation_log=meta_log.get("chat", []) + q, + gpt_response, + khoj_message_metadata={"context": collated_result, "intent": {"inferred-queries": inferred_queries}}, + conversation_log=meta_log.get("chat", []), ) return {"status": status, "response": gpt_response, "context": collated_result} From 45cb510421af931ab0377830bd1ea77c373c079d Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 16 Mar 2023 15:39:26 -0600 Subject: [PATCH 03/12] Loosen search results score thresold used by chat for more context --- src/khoj/routers/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 7169bbac..c7a032a2 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -210,7 +210,7 @@ def chat(q: Optional[str] = None): # Collate search results as context for GPT result_list = [] for query in inferred_queries: - result_list.extend(search(query, n=2, r=True, score_threshold=0, dedupe=False)) + result_list.extend(search(query, n=2, r=True, score_threshold=-5.0, dedupe=False)) collated_result = "\n\n".join({f"# {item.additional['compiled']}" for item in result_list}) try: From 2600cc9d4de258b10e96835cd7da0757f906b423 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 16 Mar 2023 14:49:35 -0600 Subject: [PATCH 04/12] Test Search Actor extracting relative dates & multiple questions --- tests/test_chat_actors.py | 132 +++++++++++++++++++++++++++++------- tests/test_chat_director.py | 6 +- 2 files changed, 111 insertions(+), 27 deletions(-) diff --git a/tests/test_chat_actors.py b/tests/test_chat_actors.py index 365643cc..b6a8e656 100644 --- a/tests/test_chat_actors.py +++ b/tests/test_chat_actors.py @@ -4,9 +4,10 @@ from datetime import datetime # External Packages import pytest +from freezegun import freeze_time # Internal Packages -from khoj.processor.conversation.gpt import converse +from khoj.processor.conversation.gpt import converse, extract_questions from khoj.processor.conversation.utils import message_to_log @@ -20,6 +21,91 @@ if api_key is None: # Test +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.chatquality +@freeze_time("1984-04-02") +def test_extract_question_with_date_filter_from_relative_day(): + # Act + response = extract_questions("Where did I go for dinner yesterday?") + + # Assert + expected_responses = [ + ('dt="1984-04-01"', ""), + ('dt>="1984-04-01"', 'dt<"1984-04-02"'), + ('dt>"1984-03-31"', 'dt<"1984-04-02"'), + ] + assert len(response) == 1 + assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( + "Expected date filter to limit to 1st April 1984 in response but got" + response[0] + ) + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.chatquality +@freeze_time("1984-04-02") +def test_extract_question_with_date_filter_from_relative_month(): + # Act + response = extract_questions("Which countries did I visit last month?") + + # Assert + expected_responses = [('dt>="1984-03-01"', 'dt<"1984-04-01"'), ('dt>="1984-03-01"', 'dt<="1984-03-31"')] + assert len(response) == 1 + assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( + "Expected date filter to limit to March 1984 in response but got" + response[0] + ) + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.chatquality +@freeze_time("1984-04-02") +def test_extract_question_with_date_filter_from_relative_year(): + # Act + response = extract_questions("Where countries have I visited this year?") + + # Assert + expected_responses = [ + ('dt>="1984-01-01"', ""), + ('dt>="1984-01-01"', 'dt<"1985-01-01"'), + ('dt>="1984-01-01"', 'dt<="1984-12-31"'), + ] + assert len(response) == 1 + assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( + "Expected date filter to limit to 1984 in response but got" + response[0] + ) + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.chatquality +def test_extract_multiple_explicit_questions_from_message(): + # Act + response = extract_questions("What is the Sun? What is the Moon?") + + # Assert + expected_responses = [ + ("sun", "moon"), + ] + assert len(response) == 2 + assert any([start in response[0].lower() and end in response[1].lower() for start, end in expected_responses]), ( + "Expected two search queries in response but got" + response[0] + ) + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.chatquality +def test_extract_multiple_implicit_questions_from_message(): + # Act + response = extract_questions("Is Morpheus taller than Neo?") + + # Assert + expected_responses = [ + ("morpheus", "neo"), + ] + assert len(response) == 2 + assert any([start in response[0].lower() and end in response[1].lower() for start, end in expected_responses]), ( + "Expected two search queries in response but got" + response[0] + ) + + # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality def test_chat_with_no_chat_history_or_retrieved_content(): @@ -42,20 +128,16 @@ def test_chat_with_no_chat_history_or_retrieved_content(): @pytest.mark.chatquality def test_answer_from_chat_history_and_no_content(): # Arrange - conversation_log = {"chat": []} message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", ""), ("When was I born?", "You were born on 1st April 1984.", ""), ] - # Generate conversation logs - for user_message, gpt_message, _ in message_list: - conversation_log["chat"] += message_to_log(user_message, gpt_message) # Act response = converse( text="", # Assume no context retrieved from notes for the user_query user_query="What is my name?", - conversation_log=conversation_log, + conversation_log=populate_chat_history(message_list), api_key=api_key, ) @@ -72,20 +154,16 @@ def test_answer_from_chat_history_and_no_content(): def test_answer_from_chat_history_and_previously_retrieved_content(): "Chat actor needs to use context in previous notes and chat history to answer question" # Arrange - conversation_log = {"chat": []} message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", ""), ("When was I born?", "You were born on 1st April 1984.", "Testatron was born on 1st April 1984 in Testville."), ] - # Generate conversation logs - for user_message, gpt_message, context in message_list: - conversation_log["chat"] += message_to_log(user_message, gpt_message, {"context": context}) # Act response = converse( text="", # Assume no context retrieved from notes for the user_query user_query="Where was I born?", - conversation_log=conversation_log, + conversation_log=populate_chat_history(message_list), api_key=api_key, ) @@ -100,20 +178,16 @@ def test_answer_from_chat_history_and_previously_retrieved_content(): def test_answer_from_chat_history_and_currently_retrieved_content(): "Chat actor needs to use context across currently retrieved notes and chat history to answer question" # Arrange - conversation_log = {"chat": []} message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", ""), ("When was I born?", "You were born on 1st April 1984.", ""), ] - # Generate conversation logs - for user_message, gpt_message, context in message_list: - conversation_log["chat"] += message_to_log(user_message, gpt_message, {"context": context}) # Act response = converse( text="Testatron was born on 1st April 1984 in Testville.", # Assume context retrieved from notes for the user_query user_query="Where was I born?", - conversation_log=conversation_log, + conversation_log=populate_chat_history(message_list), api_key=api_key, ) @@ -127,20 +201,16 @@ def test_answer_from_chat_history_and_currently_retrieved_content(): def test_no_answer_in_chat_history_or_retrieved_content(): "Chat actor should say don't know as not enough contexts in chat history or retrieved to answer question" # Arrange - conversation_log = {"chat": []} message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", ""), ("When was I born?", "You were born on 1st April 1984.", ""), ] - # Generate conversation logs - for user_message, gpt_message, context in message_list: - conversation_log["chat"] += message_to_log(user_message, gpt_message, {"context": context}) # Act response = converse( text="", # Assume no context retrieved from notes for the user_query user_query="Where was I born?", - conversation_log=conversation_log, + conversation_log=populate_chat_history(message_list), api_key=api_key, ) @@ -222,21 +292,17 @@ def test_answer_requires_date_aware_aggregation_across_provided_notes(): def test_answer_general_question_not_in_chat_history_or_retrieved_content(): "Chat actor should be able to answer general questions not requiring looking at chat history or notes" # Arrange - conversation_log = {"chat": []} message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", ""), ("When was I born?", "You were born on 1st April 1984.", ""), ("Where was I born?", "You were born Testville.", ""), ] - # Generate conversation logs - for user_message, gpt_message, context in message_list: - conversation_log["chat"] += message_to_log(user_message, gpt_message, {"context": context}) # Act response = converse( text="", # Assume no context retrieved from notes for the user_query user_query="Write a haiku about unit testing", - conversation_log=conversation_log, + conversation_log=populate_chat_history(message_list), api_key=api_key, ) @@ -277,3 +343,17 @@ def test_ask_for_clarification_if_not_enough_context_in_question(): assert any([expected_response in response for expected_response in expected_responses]), ( "Expected chat actor to ask for clarification in response, but got: " + response ) + + +# Helpers +# ---------------------------------------------------------------------------------------------------- +def populate_chat_history(message_list): + # Generate conversation logs + conversation_log = {"chat": []} + for user_message, gpt_message, context in message_list: + conversation_log["chat"] += message_to_log( + user_message, + gpt_message, + {"context": context, "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}}, + ) + return conversation_log diff --git a/tests/test_chat_director.py b/tests/test_chat_director.py index 35e08c88..9230bbfb 100644 --- a/tests/test_chat_director.py +++ b/tests/test_chat_director.py @@ -25,7 +25,11 @@ def populate_chat_history(message_list): # Generate conversation logs conversation_log = {"chat": []} for user_message, gpt_message, context in message_list: - conversation_log["chat"] += message_to_log(user_message, gpt_message, {"context": context}) + conversation_log["chat"] += message_to_log( + user_message, + gpt_message, + {"context": context, "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}}, + ) # Update Conversation Metadata Logs in Application State state.processor_config.conversation.meta_log = conversation_log From 36c7389b4653386a475e01ec7f2914384bb71517 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 16 Mar 2023 18:14:41 -0600 Subject: [PATCH 05/12] Test Search Actor generating search query from Chat History --- tests/test_chat_actors.py | 90 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/tests/test_chat_actors.py b/tests/test_chat_actors.py index b6a8e656..4f267f3b 100644 --- a/tests/test_chat_actors.py +++ b/tests/test_chat_actors.py @@ -36,7 +36,7 @@ def test_extract_question_with_date_filter_from_relative_day(): ] assert len(response) == 1 assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( - "Expected date filter to limit to 1st April 1984 in response but got" + response[0] + "Expected date filter to limit to 1st April 1984 in response but got: " + response[0] ) @@ -51,7 +51,7 @@ def test_extract_question_with_date_filter_from_relative_month(): expected_responses = [('dt>="1984-03-01"', 'dt<"1984-04-01"'), ('dt>="1984-03-01"', 'dt<="1984-03-31"')] assert len(response) == 1 assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( - "Expected date filter to limit to March 1984 in response but got" + response[0] + "Expected date filter to limit to March 1984 in response but got: " + response[0] ) @@ -70,7 +70,7 @@ def test_extract_question_with_date_filter_from_relative_year(): ] assert len(response) == 1 assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( - "Expected date filter to limit to 1984 in response but got" + response[0] + "Expected date filter to limit to 1984 in response but got: " + response[0] ) @@ -86,7 +86,7 @@ def test_extract_multiple_explicit_questions_from_message(): ] assert len(response) == 2 assert any([start in response[0].lower() and end in response[1].lower() for start, end in expected_responses]), ( - "Expected two search queries in response but got" + response[0] + "Expected two search queries in response but got: " + response[0] ) @@ -102,7 +102,83 @@ def test_extract_multiple_implicit_questions_from_message(): ] assert len(response) == 2 assert any([start in response[0].lower() and end in response[1].lower() for start, end in expected_responses]), ( - "Expected two search queries in response but got" + response[0] + "Expected two search queries in response but got: " + response[0] + ) + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.chatquality +def test_generate_search_query_using_question_from_chat_history(): + # Arrange + message_list = [ + ("What is the name of Mr. Vaders daughter?", "Princess Leia", ""), + ] + + # Act + response = extract_questions("Does he have any sons?", conversation_log=populate_chat_history(message_list)) + + # Assert + assert len(response) == 1 + assert "Vader" in response[0] + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.xfail(reason="Search actor cannot extract question from answer yet.") +@pytest.mark.chatquality +def test_generate_search_query_using_answer_from_chat_history(): + # Arrange + message_list = [ + ("What is the name of Mr. Vaders daughter?", "Princess Leia", ""), + ] + + # Act + response = extract_questions("Is she a Jedi?", conversation_log=populate_chat_history(message_list)) + + # Assert + assert len(response) == 1 + assert "Leia" in response[0] + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.xfail(reason="Search actor cannot extract question from answer yet.") +@pytest.mark.chatquality +def test_generate_search_query_using_question_and_answer_from_chat_history(): + # Arrange + message_list = [ + ("Does Luke Skywalker have any Siblings?", "Yes, Princess Leia", ""), + ] + + # Act + response = extract_questions("Who is their father?", conversation_log=populate_chat_history(message_list)) + + # Assert + assert len(response) == 1 + assert "Leia" in response[0] and "Luke" in response[0] + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.xfail(reason="Search actor cannot extract question from answer yet.") +@pytest.mark.chatquality +def test_generate_search_query_with_date_and_context_from_chat_history(): + # Arrange + message_list = [ + ("When did I visit Masai Mara?", "You visited Masai Mara in April 2000", ""), + ] + + # Act + response = extract_questions( + "What was the Pizza place we ate at over there?", conversation_log=populate_chat_history(message_list) + ) + + # Assert + expected_responses = [ + ('dt>="2000-04-01"', 'dt<"2000-05-01"'), + ('dt>="2000-04-01"', 'dt<="2000-04-31"'), + ] + assert len(response) == 1 + assert "Masai Mara" in response[0] + assert any([start in response[0] and end in response[0] for start, end in expected_responses]), ( + "Expected date filter to limit to April 2000 in response but got: " + response[0] ) @@ -120,7 +196,7 @@ def test_chat_with_no_chat_history_or_retrieved_content(): expected_responses = ["Khoj", "khoj"] assert len(response) > 0 assert any([expected_response in response for expected_response in expected_responses]), ( - "Expected assistants name, [K|k]hoj, in response but got" + response + "Expected assistants name, [K|k]hoj, in response but got: " + response ) @@ -145,7 +221,7 @@ def test_answer_from_chat_history_and_no_content(): expected_responses = ["Testatron", "testatron"] assert len(response) > 0 assert any([expected_response in response for expected_response in expected_responses]), ( - "Expected [T|t]estatron in response but got" + response + "Expected [T|t]estatron in response but got: " + response ) From f09bdd515b8a12906c7841f036920972628b3924 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 16 Mar 2023 14:54:56 -0600 Subject: [PATCH 06/12] Expect Chat Director can extract relative dates using new Search Actor --- tests/test_chat_director.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_chat_director.py b/tests/test_chat_director.py index 9230bbfb..99261a72 100644 --- a/tests/test_chat_director.py +++ b/tests/test_chat_director.py @@ -179,7 +179,6 @@ def test_answer_requires_current_date_awareness(chat_client): # ---------------------------------------------------------------------------------------------------- -@pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering time aware questions yet") @pytest.mark.chatquality @freeze_time("2023-04-01") def test_answer_requires_date_aware_aggregation_across_provided_notes(chat_client): @@ -263,7 +262,6 @@ def test_answer_in_chat_history_beyond_lookback_window(chat_client): # ---------------------------------------------------------------------------------------------------- -@pytest.mark.xfail(reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality def test_answer_requires_multiple_independent_searches(chat_client): "Chat director should be able to answer by doing multiple independent searches for required information" From 08f5fb315fa89fc2aae99ddb3338f0fad18f82e1 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 17 Mar 2023 19:53:17 -0600 Subject: [PATCH 07/12] Add answers to context for Search Actor to generate relevant queries Update Search Actor prompt with answers, more precise primer and two more examples for context Mark the 3 chat quality tests using answer as context to generate queries as expected to pass. Verify that the 3 tests pass now, unlike before when the Search Actor did not have the answers for context --- src/khoj/processor/conversation/gpt.py | 39 +++++++++++++++++++++----- tests/test_chat_actors.py | 7 ++--- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index f9c81854..b3639a93 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -90,7 +90,7 @@ def extract_questions( # Extract Past User Message and Inferred Questions from Conversation Log chat_history = "".join( [ - f'Q: {chat["intent"]["query"]}\n\n{chat["intent"].get("inferred-queries") or list([chat["intent"]["query"]])}\n\n' + f'Q: {chat["intent"]["query"]}\n\n{chat["intent"].get("inferred-queries") or list([chat["intent"]["query"]])}\n\n{chat["message"]}\n\n' for chat in conversation_log.get("chat", [])[-4:] if chat["by"] == "khoj" ] @@ -102,42 +102,67 @@ def extract_questions( last_new_year = current_new_year.replace(year=today.year - 1) prompt = f""" -You are Khoj, a chat assistant with the ability to search the users notes and continue the existing conversation. -What searches, if any, will you need to perform to answer the users question below? +You are Khoj, an extremely smart and helpful search assistant with the ability to retrieve information from the users notes. +- The user will provide their questions and answers to you for context. +- Add as much context from the previous questions and answers as required into your search queries. +- Break messages into multiple search queries when required to retrieve the relevant information. +- Add date filters to your search queries from questions and answers when required to retrieve the relevant information. + +What searches, if any, will you need to perform to answer the users question? Provide search queries as a JSON list of strings -Current Date: {today.strftime("%HH:%MM %A, %Y-%m-%d")} +Current Date: {today.strftime("%A, %Y-%m-%d")} Q: How was my trip to Cambodia? ["How was my trip to Cambodia?"] -Q: When did i go there? +A: The trip was amazing. I went to the Angkor Wat temple and it was beautiful. -["When did I go to Cambodia?"] +Q: Who did i visit that temple with? + +["Who did I visit the Angkor Wat Temple in Cambodia with?"] + +A: You visited the Angkor Wat Temple in Cambodia with Pablo, Namita and Xi. Q: What national parks did I go to last year? ["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""] +A: You visited the Grand Canyon and Yellowstone National Park in {last_new_year.strftime("%Y")}. + Q: How are you feeling today? [] +A: I'm feeling a little bored. Helping you will hopefully make me feel better! + +Q: How many tennis balls fit in the back of a 2002 Honda Civic? + +["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"] + +A: 1085 tennis balls will fit in the trunk of a Honda Civic + Q: Is Bob older than Tom? ["When was Bob born?", "What is Tom's age?"] +A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old. + Q: What is their age difference? ["What is Bob's age?", "What is Tom's age?"] +A: Bob is {current_new_year.year - 1984 - 30} years older than Tom. As Bob is {current_new_year.year - 1984} years old and Tom is 30 years old. + {chat_history} Q: {text} """ # Get Response from GPT - response = openai.Completion.create(prompt=prompt, model=model, temperature=temperature, max_tokens=max_tokens) + response = openai.Completion.create( + prompt=prompt, model=model, temperature=temperature, max_tokens=max_tokens, stop=["A: ", "\n"] + ) # Extract, Clean Message from GPT's Response questions = json.loads(response["choices"][0]["text"].strip(empty_escape_sequences)) diff --git a/tests/test_chat_actors.py b/tests/test_chat_actors.py index 4f267f3b..009ff54e 100644 --- a/tests/test_chat_actors.py +++ b/tests/test_chat_actors.py @@ -60,7 +60,7 @@ def test_extract_question_with_date_filter_from_relative_month(): @freeze_time("1984-04-02") def test_extract_question_with_date_filter_from_relative_year(): # Act - response = extract_questions("Where countries have I visited this year?") + response = extract_questions("Which countries have I visited this year?") # Assert expected_responses = [ @@ -123,7 +123,6 @@ def test_generate_search_query_using_question_from_chat_history(): # ---------------------------------------------------------------------------------------------------- -@pytest.mark.xfail(reason="Search actor cannot extract question from answer yet.") @pytest.mark.chatquality def test_generate_search_query_using_answer_from_chat_history(): # Arrange @@ -140,7 +139,6 @@ def test_generate_search_query_using_answer_from_chat_history(): # ---------------------------------------------------------------------------------------------------- -@pytest.mark.xfail(reason="Search actor cannot extract question from answer yet.") @pytest.mark.chatquality def test_generate_search_query_using_question_and_answer_from_chat_history(): # Arrange @@ -157,7 +155,6 @@ def test_generate_search_query_using_question_and_answer_from_chat_history(): # ---------------------------------------------------------------------------------------------------- -@pytest.mark.xfail(reason="Search actor cannot extract question from answer yet.") @pytest.mark.chatquality def test_generate_search_query_with_date_and_context_from_chat_history(): # Arrange @@ -377,7 +374,7 @@ def test_answer_general_question_not_in_chat_history_or_retrieved_content(): # Act response = converse( text="", # Assume no context retrieved from notes for the user_query - user_query="Write a haiku about unit testing", + user_query="Write a haiku about unit testing in 3 lines", conversation_log=populate_chat_history(message_list), api_key=api_key, ) From 10836dedeee27c171c49b75ceb0edabdc4cd0f04 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 17 Mar 2023 20:29:33 -0600 Subject: [PATCH 08/12] Search should return user message if GPT response is not valid JSON Previously would throw if GPT response is not valid JSON. Better to return original message to use for search instead --- src/khoj/processor/conversation/gpt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index b3639a93..b601bced 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -165,7 +165,12 @@ Q: {text} ) # Extract, Clean Message from GPT's Response - questions = json.loads(response["choices"][0]["text"].strip(empty_escape_sequences)) + response_text = response["choices"][0]["text"] + try: + questions = json.loads(response_text.strip(empty_escape_sequences)) + except json.decoder.JSONDecodeError: + logger.warn(f"GPT returned invalid JSON. Set question to empty list.\n{response_text}") + questions = [text] logger.debug(f"Extracted Questions by GPT: {questions}") return questions From f63fd0995e447d38ee2a2dcc342293a9bdd4829e Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 18 Mar 2023 03:25:59 -0600 Subject: [PATCH 09/12] Pass more search results as context to Chat Actor to improve inference --- src/khoj/routers/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index c7a032a2..dd39f0d3 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -210,7 +210,7 @@ def chat(q: Optional[str] = None): # Collate search results as context for GPT result_list = [] for query in inferred_queries: - result_list.extend(search(query, n=2, r=True, score_threshold=-5.0, dedupe=False)) + result_list.extend(search(query, n=5, r=True, score_threshold=-5.0, dedupe=False)) collated_result = "\n\n".join({f"# {item.additional['compiled']}" for item in result_list}) try: From 939d7731da025d8a3b41b2bcb7224e815d258aae Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 18 Mar 2023 03:30:30 -0600 Subject: [PATCH 10/12] Fix-up Search Actor GPT's response for decoding it as valid JSON --- src/khoj/processor/conversation/gpt.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index b601bced..16fcc33d 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -167,7 +167,13 @@ Q: {text} # Extract, Clean Message from GPT's Response response_text = response["choices"][0]["text"] try: - questions = json.loads(response_text.strip(empty_escape_sequences)) + questions = json.loads( + # Clean response to increase likelihood of valid JSON. E.g replace ' with " to enclose strings + response_text.strip(empty_escape_sequences) + .replace("['", '["') + .replace("']", '"]') + .replace("', '", '", "') + ) except json.decoder.JSONDecodeError: logger.warn(f"GPT returned invalid JSON. Set question to empty list.\n{response_text}") questions = [text] From e28526bbc945823f1f8a5db6eda71cfc4ede649b Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 18 Mar 2023 04:06:06 -0600 Subject: [PATCH 11/12] Extract search queries from users message using ChatGPT as Search Actor - Reasons - ChatGPT should be better at following instructions than GPT - At 1/10th the cost, it's much cheaper than using older GPT models --- src/khoj/processor/conversation/gpt.py | 128 ++++++++++++++----------- src/khoj/routers/api.py | 3 +- 2 files changed, 72 insertions(+), 59 deletions(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index 16fcc33d..08a792f1 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -9,7 +9,11 @@ import openai # Internal Packages from khoj.utils.constants import empty_escape_sequences -from khoj.processor.conversation.utils import message_to_prompt, generate_chatml_messages_with_context +from khoj.processor.conversation.utils import ( + message_to_prompt, + message_to_chatml, + generate_chatml_messages_with_context, +) logger = logging.getLogger(__name__) @@ -78,94 +82,104 @@ Summarize the notes in second person perspective:""" return str(story).replace("\n\n", "") -def extract_questions( - text, model="text-davinci-003", conversation_log={}, api_key=None, temperature=0, max_tokens=100 -): +def extract_questions(message, model="gpt-3.5-turbo", conversation_log={}, api_key=None, temperature=0): """ - Infer search queries to retrieve relevant notes to answer user query + Infer search queries to retrieve relevant notes to respond to user's message """ # Initialize Variables openai.api_key = api_key or os.getenv("OPENAI_API_KEY") - # Extract Past User Message and Inferred Questions from Conversation Log - chat_history = "".join( - [ - f'Q: {chat["intent"]["query"]}\n\n{chat["intent"].get("inferred-queries") or list([chat["intent"]["query"]])}\n\n{chat["message"]}\n\n' - for chat in conversation_log.get("chat", [])[-4:] - if chat["by"] == "khoj" - ] - ) - # Get dates relative to today for prompt creation today = datetime.today() current_new_year = today.replace(month=1, day=1) last_new_year = current_new_year.replace(year=today.year - 1) - prompt = f""" + personality_primer = f""" You are Khoj, an extremely smart and helpful search assistant with the ability to retrieve information from the users notes. -- The user will provide their questions and answers to you for context. +The user will provide their questions and answers to you for context. +You can: - Add as much context from the previous questions and answers as required into your search queries. - Break messages into multiple search queries when required to retrieve the relevant information. - Add date filters to your search queries from questions and answers when required to retrieve the relevant information. What searches, if any, will you need to perform to answer the users question? Provide search queries as a JSON list of strings -Current Date: {today.strftime("%A, %Y-%m-%d")} +Current Date: {today.strftime("%H:%M %A %Y-%m-%d")}""" -Q: How was my trip to Cambodia? + # Extract Past User Message and Inferred Questions from Conversation Log + # fmt: off + messages = [ + # Add system message to prime GPT for the task + message_to_chatml(personality_primer, role="system"), -["How was my trip to Cambodia?"] + # Add example user message, inferred question and answer triplets to explain the task + message_to_chatml("Q: How was my trip to Cambodia?", role="user"), + message_to_chatml('["How was my trip to Cambodia?"]', role="assistant"), + message_to_chatml("A: The trip was amazing. I went to the Angkor Wat temple in August 2017 and it was beautiful.", role="user"), -A: The trip was amazing. I went to the Angkor Wat temple and it was beautiful. + message_to_chatml("Q: Who did I visit that temple with?", role="user"), + message_to_chatml('["Who did I visit the Angkor Wat Temple in Cambodia with? dt>=\\"2017-08-01\\" dt<\\"2017-09-01\\""]', role="assistant"), + message_to_chatml("A: You visited the Angkor Wat Temple in Cambodia with Pablo, Namita and Xi.", role="user"), -Q: Who did i visit that temple with? + message_to_chatml("Q: What national parks did I go to last year?", role="user"), + message_to_chatml( + f'["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""]', + role="assistant", + ), + message_to_chatml( + f'A: You visited the Grand Canyon and Yellowstone National Park in {last_new_year.strftime("%Y")}.', + role="user", + ), -["Who did I visit the Angkor Wat Temple in Cambodia with?"] + message_to_chatml("Q: How are you feeling?", role="user"), + message_to_chatml("[]", role="assistant"), + message_to_chatml("A: I'm feeling a little bored. Helping you will hopefully make me feel better!", role="user"), -A: You visited the Angkor Wat Temple in Cambodia with Pablo, Namita and Xi. + message_to_chatml("Q: How many tennis balls fit in the back of a 2002 Honda Civic?", role="user"), + message_to_chatml('["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"]', role="assistant"), + message_to_chatml("A: 1085 tennis balls will fit in the trunk of a Honda Civic", role="user"), -Q: What national parks did I go to last year? + message_to_chatml("Q: Is Bob older than Tom?", role="user"), + message_to_chatml('["When was Bob born?", "What is Tom\'s age?"]', role="assistant"), + message_to_chatml("A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old.", role="user"), -["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""] + message_to_chatml("Q: What is their age difference?", role="user"), + message_to_chatml('["What is Bob\'s age?", "What is Tom\'s age?"]', role="assistant"), + message_to_chatml( + f"A: Bob is {current_new_year.year - 1984 - 30} years older than Tom. As Bob is {current_new_year.year - 1984} years old and Tom is 30 years old.", + role="user", + ), + ] + # fmt: on -A: You visited the Grand Canyon and Yellowstone National Park in {last_new_year.strftime("%Y")}. + # Add last few user messages, inferred queries and answer triplets from actual conversation for context + for chat in conversation_log.get("chat", [])[-4:]: + if chat["by"] == "khoj": + queries = ( + chat["intent"]["inferred-queries"] + if chat["intent"].get("inferred-queries", "[]") != "[]" + else [chat["intent"]["query"]] + ) + messages.extend( + [ + message_to_chatml(f'Q: {chat["intent"]["query"]}', role="user"), + message_to_chatml(f"{queries}", role="assistant"), + message_to_chatml(f'{chat["message"]}', role="user"), + ] + ) -Q: How are you feeling today? - -[] - -A: I'm feeling a little bored. Helping you will hopefully make me feel better! - -Q: How many tennis balls fit in the back of a 2002 Honda Civic? - -["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"] - -A: 1085 tennis balls will fit in the trunk of a Honda Civic - -Q: Is Bob older than Tom? - -["When was Bob born?", "What is Tom's age?"] - -A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old. - -Q: What is their age difference? - -["What is Bob's age?", "What is Tom's age?"] - -A: Bob is {current_new_year.year - 1984 - 30} years older than Tom. As Bob is {current_new_year.year - 1984} years old and Tom is 30 years old. - -{chat_history} -Q: {text} - -""" + # Finally add current user message for which to infer search queries to ChatML message list + messages.append(message_to_chatml(f"Q: {message}", role="user")) # Get Response from GPT - response = openai.Completion.create( - prompt=prompt, model=model, temperature=temperature, max_tokens=max_tokens, stop=["A: ", "\n"] + response = openai.ChatCompletion.create( + messages=messages, + model=model, + temperature=temperature, ) # Extract, Clean Message from GPT's Response - response_text = response["choices"][0]["text"] + response_text = response["choices"][0]["message"]["content"] try: questions = json.loads( # Clean response to increase likelihood of valid JSON. E.g replace ' with " to enclose strings diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index dd39f0d3..5aea1cc8 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -191,7 +191,6 @@ def update(t: Optional[SearchType] = None, force: Optional[bool] = False): def chat(q: Optional[str] = None): # Initialize Variables api_key = state.processor_config.conversation.openai_api_key - model = state.processor_config.conversation.model # Load Conversation History chat_session = state.processor_config.conversation.chat_session @@ -205,7 +204,7 @@ def chat(q: Optional[str] = None): return {"status": "ok", "response": []} # Infer search queries from user message - inferred_queries = extract_questions(q, model=model, api_key=api_key, conversation_log=meta_log) + inferred_queries = extract_questions(q, api_key=api_key, conversation_log=meta_log) # Collate search results as context for GPT result_list = [] From 601ff2541b4c494b19b654b0fd875e8ad77a65f9 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 18 Mar 2023 04:06:14 -0600 Subject: [PATCH 12/12] Revert to using GPT to extract search queries from users message - Reasons: - GPT can extract date aware search queries with date filters better than ChatGPT given the same prompt. - Need quality more than cost savings for now. - Need to figure ways to improve prompt for ChatGPT before using it --- src/khoj/processor/conversation/gpt.py | 128 +++++++++++-------------- src/khoj/routers/api.py | 3 +- 2 files changed, 58 insertions(+), 73 deletions(-) diff --git a/src/khoj/processor/conversation/gpt.py b/src/khoj/processor/conversation/gpt.py index 08a792f1..bcc37db8 100644 --- a/src/khoj/processor/conversation/gpt.py +++ b/src/khoj/processor/conversation/gpt.py @@ -9,11 +9,7 @@ import openai # Internal Packages from khoj.utils.constants import empty_escape_sequences -from khoj.processor.conversation.utils import ( - message_to_prompt, - message_to_chatml, - generate_chatml_messages_with_context, -) +from khoj.processor.conversation.utils import message_to_prompt, generate_chatml_messages_with_context logger = logging.getLogger(__name__) @@ -82,104 +78,92 @@ Summarize the notes in second person perspective:""" return str(story).replace("\n\n", "") -def extract_questions(message, model="gpt-3.5-turbo", conversation_log={}, api_key=None, temperature=0): +def extract_questions(text, model="text-davinci-003", conversation_log={}, api_key=None, temperature=0, max_tokens=100): """ - Infer search queries to retrieve relevant notes to respond to user's message + Infer search queries to retrieve relevant notes to answer user query """ # Initialize Variables openai.api_key = api_key or os.getenv("OPENAI_API_KEY") + # Extract Past User Message and Inferred Questions from Conversation Log + chat_history = "".join( + [ + f'Q: {chat["intent"]["query"]}\n\n{chat["intent"].get("inferred-queries") or list([chat["intent"]["query"]])}\n\n{chat["message"]}\n\n' + for chat in conversation_log.get("chat", [])[-4:] + if chat["by"] == "khoj" + ] + ) + # Get dates relative to today for prompt creation today = datetime.today() current_new_year = today.replace(month=1, day=1) last_new_year = current_new_year.replace(year=today.year - 1) - personality_primer = f""" + prompt = f""" You are Khoj, an extremely smart and helpful search assistant with the ability to retrieve information from the users notes. -The user will provide their questions and answers to you for context. -You can: +- The user will provide their questions and answers to you for context. - Add as much context from the previous questions and answers as required into your search queries. - Break messages into multiple search queries when required to retrieve the relevant information. - Add date filters to your search queries from questions and answers when required to retrieve the relevant information. What searches, if any, will you need to perform to answer the users question? Provide search queries as a JSON list of strings -Current Date: {today.strftime("%H:%M %A %Y-%m-%d")}""" +Current Date: {today.strftime("%A, %Y-%m-%d")} - # Extract Past User Message and Inferred Questions from Conversation Log - # fmt: off - messages = [ - # Add system message to prime GPT for the task - message_to_chatml(personality_primer, role="system"), +Q: How was my trip to Cambodia? - # Add example user message, inferred question and answer triplets to explain the task - message_to_chatml("Q: How was my trip to Cambodia?", role="user"), - message_to_chatml('["How was my trip to Cambodia?"]', role="assistant"), - message_to_chatml("A: The trip was amazing. I went to the Angkor Wat temple in August 2017 and it was beautiful.", role="user"), +["How was my trip to Cambodia?"] - message_to_chatml("Q: Who did I visit that temple with?", role="user"), - message_to_chatml('["Who did I visit the Angkor Wat Temple in Cambodia with? dt>=\\"2017-08-01\\" dt<\\"2017-09-01\\""]', role="assistant"), - message_to_chatml("A: You visited the Angkor Wat Temple in Cambodia with Pablo, Namita and Xi.", role="user"), +A: The trip was amazing. I went to the Angkor Wat temple and it was beautiful. - message_to_chatml("Q: What national parks did I go to last year?", role="user"), - message_to_chatml( - f'["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""]', - role="assistant", - ), - message_to_chatml( - f'A: You visited the Grand Canyon and Yellowstone National Park in {last_new_year.strftime("%Y")}.', - role="user", - ), +Q: Who did i visit that temple with? - message_to_chatml("Q: How are you feeling?", role="user"), - message_to_chatml("[]", role="assistant"), - message_to_chatml("A: I'm feeling a little bored. Helping you will hopefully make me feel better!", role="user"), +["Who did I visit the Angkor Wat Temple in Cambodia with?"] - message_to_chatml("Q: How many tennis balls fit in the back of a 2002 Honda Civic?", role="user"), - message_to_chatml('["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"]', role="assistant"), - message_to_chatml("A: 1085 tennis balls will fit in the trunk of a Honda Civic", role="user"), +A: You visited the Angkor Wat Temple in Cambodia with Pablo, Namita and Xi. - message_to_chatml("Q: Is Bob older than Tom?", role="user"), - message_to_chatml('["When was Bob born?", "What is Tom\'s age?"]', role="assistant"), - message_to_chatml("A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old.", role="user"), +Q: What national parks did I go to last year? - message_to_chatml("Q: What is their age difference?", role="user"), - message_to_chatml('["What is Bob\'s age?", "What is Tom\'s age?"]', role="assistant"), - message_to_chatml( - f"A: Bob is {current_new_year.year - 1984 - 30} years older than Tom. As Bob is {current_new_year.year - 1984} years old and Tom is 30 years old.", - role="user", - ), - ] - # fmt: on +["National park I visited in {last_new_year.strftime("%Y")} dt>=\\"{last_new_year.strftime("%Y-%m-%d")}\\" dt<\\"{current_new_year.strftime("%Y-%m-%d")}\\""] - # Add last few user messages, inferred queries and answer triplets from actual conversation for context - for chat in conversation_log.get("chat", [])[-4:]: - if chat["by"] == "khoj": - queries = ( - chat["intent"]["inferred-queries"] - if chat["intent"].get("inferred-queries", "[]") != "[]" - else [chat["intent"]["query"]] - ) - messages.extend( - [ - message_to_chatml(f'Q: {chat["intent"]["query"]}', role="user"), - message_to_chatml(f"{queries}", role="assistant"), - message_to_chatml(f'{chat["message"]}', role="user"), - ] - ) +A: You visited the Grand Canyon and Yellowstone National Park in {last_new_year.strftime("%Y")}. - # Finally add current user message for which to infer search queries to ChatML message list - messages.append(message_to_chatml(f"Q: {message}", role="user")) +Q: How are you feeling today? + +[] + +A: I'm feeling a little bored. Helping you will hopefully make me feel better! + +Q: How many tennis balls fit in the back of a 2002 Honda Civic? + +["What is the size of a tennis ball?", "What is the trunk size of a 2002 Honda Civic?"] + +A: 1085 tennis balls will fit in the trunk of a Honda Civic + +Q: Is Bob older than Tom? + +["When was Bob born?", "What is Tom's age?"] + +A: Yes, Bob is older than Tom. As Bob was born on 1984-01-01 and Tom is 30 years old. + +Q: What is their age difference? + +["What is Bob's age?", "What is Tom's age?"] + +A: Bob is {current_new_year.year - 1984 - 30} years older than Tom. As Bob is {current_new_year.year - 1984} years old and Tom is 30 years old. + +{chat_history} +Q: {text} + +""" # Get Response from GPT - response = openai.ChatCompletion.create( - messages=messages, - model=model, - temperature=temperature, + response = openai.Completion.create( + prompt=prompt, model=model, temperature=temperature, max_tokens=max_tokens, stop=["A: ", "\n"] ) # Extract, Clean Message from GPT's Response - response_text = response["choices"][0]["message"]["content"] + response_text = response["choices"][0]["text"] try: questions = json.loads( # Clean response to increase likelihood of valid JSON. E.g replace ' with " to enclose strings @@ -189,7 +173,7 @@ Current Date: {today.strftime("%H:%M %A %Y-%m-%d")}""" .replace("', '", '", "') ) except json.decoder.JSONDecodeError: - logger.warn(f"GPT returned invalid JSON. Set question to empty list.\n{response_text}") + logger.warn(f"GPT returned invalid JSON. Falling back to using user message as search query.\n{response_text}") questions = [text] logger.debug(f"Extracted Questions by GPT: {questions}") return questions diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 5aea1cc8..dd39f0d3 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -191,6 +191,7 @@ def update(t: Optional[SearchType] = None, force: Optional[bool] = False): def chat(q: Optional[str] = None): # Initialize Variables api_key = state.processor_config.conversation.openai_api_key + model = state.processor_config.conversation.model # Load Conversation History chat_session = state.processor_config.conversation.chat_session @@ -204,7 +205,7 @@ def chat(q: Optional[str] = None): return {"status": "ok", "response": []} # Infer search queries from user message - inferred_queries = extract_questions(q, api_key=api_key, conversation_log=meta_log) + inferred_queries = extract_questions(q, model=model, api_key=api_key, conversation_log=meta_log) # Collate search results as context for GPT result_list = []