2023-03-15 00:44:16 +01:00
|
|
|
import os
|
|
|
|
from datetime import datetime
|
|
|
|
|
2023-07-08 00:18:55 +02:00
|
|
|
import freezegun
|
2023-12-28 13:34:02 +01:00
|
|
|
import pytest
|
2023-03-16 21:49:35 +01:00
|
|
|
from freezegun import freeze_time
|
2021-11-26 12:53:03 +01:00
|
|
|
|
2023-07-27 01:27:08 +02:00
|
|
|
from khoj.processor.conversation.openai.gpt import converse, extract_questions
|
2023-03-16 19:20:33 +01:00
|
|
|
from khoj.processor.conversation.utils import message_to_log
|
2024-03-13 12:19:13 +01:00
|
|
|
from khoj.routers.helpers import (
|
|
|
|
aget_relevant_information_sources,
|
|
|
|
aget_relevant_output_modes,
|
|
|
|
generate_online_subqueries,
|
Part 2: Add web UI updates for basic agent interactions (#675)
* Initial pass at backend changes to support agents
- Add a db model for Agents, attaching them to conversations
- When an agent is added to a conversation, override the system prompt to tweak the instructions
- Agents can be configured with prompt modification, model specification, a profile picture, and other things
- Admin-configured models will not be editable by individual users
- Add unit tests to verify agent behavior. Unit tests demonstrate imperfect adherence to prompt specifications
* Customize default behaviors for conversations without agents or with default agents
* Add a new web client route for viewing all agents
* Use agent_id for getting correct agent
* Add web UI views for agents
- Add a page to view all agents
- Add slugs to manage agents
- Add a view to view single agent
- Display active agent when in chat window
- Fix post-login redirect issue
* Fix agent view
* Spruce up the 404 page and improve the overall layout for agents pages
* Create chat actor for directly reading webpages based on user message
- Add prompt for the read webpages chat actor to extract, infer
webpage links
- Make chat actor infer or extract webpage to read directly from user
message
- Rename previous read_webpage function to more narrow
read_webpage_at_url function
* Rename agents_page -> agent_page
* Fix unit test for adding the filename to the compiled markdown entry
* Fix layout of agent, agents pages
* Merge migrations
* Let the name, slug of the default agent be Khoj, khoj
* Fix chat-related unit tests
* Add webpage chat command for read web pages requested by user
Update auto chat command inference prompt to show example of when to
use webpage chat command (i.e when url is directly provided in link)
* Support webpage command in chat API
- Fallback to use webpage when SERPER not setup and online command was
attempted
- Do not stop responding if can't retrieve online results. Try to
respond without the online context
* Test select webpage as data source and extract web urls chat actors
* Tweak prompts to extract information from webpages, online results
- Show more of the truncated messages for debugging context
- Update Khoj personality prompt to encourage it to remember it's capabilities
* Rename extract_content online results field to webpages
* Parallelize simple webpage read and extractor
Similar to what is being done with search_online with olostep
* Pass multiple webpages with their urls in online results context
Previously even if MAX_WEBPAGES_TO_READ was > 1, only 1 extracted
content would ever be passed.
URL of the extracted webpage content wasn't passed to clients in
online results context. This limited them from being rendered
* Render webpage read in chat response references on Web, Desktop apps
* Time chat actor responses & chat api request start for perf analysis
* Increase the keep alive timeout in the main application for testing
* Do not pipe access/error logs to separate files. Flow to stdout/stderr
* [Temp] Reduce to 1 gunicorn worker
* Change prod docker image to use jammy, rather than nvidia base image
* Use Khoj icon when Khoj web is installed on iOS as a PWA
* Make slug required for agents
* Simplify calling logic and prevent agent access for unauthenticated users
* Standardize to use personality over tuning in agent nomenclature
* Make filtering logic more stringent for accessible agents and remove unused method:
* Format chat message query
---------
Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
2024-03-26 13:43:24 +01:00
|
|
|
infer_webpage_urls,
|
2024-03-13 12:19:13 +01:00
|
|
|
)
|
|
|
|
from khoj.utils.helpers import ConversationCommand
|
2021-11-26 12:53:03 +01:00
|
|
|
|
2023-01-09 04:09:24 +01:00
|
|
|
# Initialize variables for tests
|
2023-03-16 19:20:33 +01:00
|
|
|
api_key = os.getenv("OPENAI_API_KEY")
|
|
|
|
if api_key is None:
|
|
|
|
pytest.skip(
|
|
|
|
reason="Set OPENAI_API_KEY environment variable to run tests below. Get OpenAI API key from https://platform.openai.com/account/api-keys",
|
|
|
|
allow_module_level=True,
|
2023-02-17 17:04:26 +01:00
|
|
|
)
|
2021-11-26 12:53:03 +01:00
|
|
|
|
2023-07-08 00:18:55 +02:00
|
|
|
freezegun.configure(extend_ignore_list=["transformers"])
|
|
|
|
|
2021-11-26 12:53:03 +01:00
|
|
|
|
2023-03-16 19:20:33 +01:00
|
|
|
# Test
|
2023-03-16 21:49:35 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.chatquality
|
2024-02-05 15:32:54 +01:00
|
|
|
@freeze_time("1984-04-02", ignore=["transformers"])
|
2023-03-16 21:49:35 +01:00
|
|
|
def test_extract_question_with_date_filter_from_relative_day():
|
|
|
|
# Act
|
|
|
|
response = extract_questions("Where did I go for dinner yesterday?")
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
expected_responses = [
|
2023-07-08 02:13:47 +02:00
|
|
|
("dt='1984-04-01'", ""),
|
|
|
|
("dt>='1984-04-01'", "dt<'1984-04-02'"),
|
|
|
|
("dt>'1984-03-31'", "dt<'1984-04-02'"),
|
2023-03-16 21:49:35 +01:00
|
|
|
]
|
|
|
|
assert len(response) == 1
|
|
|
|
assert any([start in response[0] and end in response[0] for start, end in expected_responses]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"Expected date filter to limit to 1st April 1984 in response but got: " + response[0]
|
2023-03-16 21:49:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.chatquality
|
2024-02-05 15:32:54 +01:00
|
|
|
@freeze_time("1984-04-02", ignore=["transformers"])
|
2023-03-16 21:49:35 +01:00
|
|
|
def test_extract_question_with_date_filter_from_relative_month():
|
|
|
|
# Act
|
|
|
|
response = extract_questions("Which countries did I visit last month?")
|
|
|
|
|
|
|
|
# Assert
|
2023-07-08 02:13:47 +02:00
|
|
|
expected_responses = [("dt>='1984-03-01'", "dt<'1984-04-01'"), ("dt>='1984-03-01'", "dt<='1984-03-31'")]
|
2023-03-16 21:49:35 +01:00
|
|
|
assert len(response) == 1
|
|
|
|
assert any([start in response[0] and end in response[0] for start, end in expected_responses]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"Expected date filter to limit to March 1984 in response but got: " + response[0]
|
2023-03-16 21:49:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.chatquality
|
2024-02-05 15:32:54 +01:00
|
|
|
@freeze_time("1984-04-02", ignore=["transformers"])
|
2023-03-16 21:49:35 +01:00
|
|
|
def test_extract_question_with_date_filter_from_relative_year():
|
|
|
|
# Act
|
2023-03-18 02:53:17 +01:00
|
|
|
response = extract_questions("Which countries have I visited this year?")
|
2023-03-16 21:49:35 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
expected_responses = [
|
2023-07-08 02:13:47 +02:00
|
|
|
("dt>='1984-01-01'", ""),
|
|
|
|
("dt>='1984-01-01'", "dt<'1985-01-01'"),
|
|
|
|
("dt>='1984-01-01'", "dt<='1984-12-31'"),
|
2023-03-16 21:49:35 +01:00
|
|
|
]
|
|
|
|
assert len(response) == 1
|
|
|
|
assert any([start in response[0] and end in response[0] for start, end in expected_responses]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"Expected date filter to limit to 1984 in response but got: " + response[0]
|
2023-03-16 21:49:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@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]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"Expected two search queries in response but got: " + response[0]
|
2023-03-16 21:49:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@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]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"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 = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("What is the name of Mr. Vader's daughter?", "Princess Leia", []),
|
2023-03-17 01:14:41 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# 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.chatquality
|
|
|
|
def test_generate_search_query_using_answer_from_chat_history():
|
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("What is the name of Mr. Vader's daughter?", "Princess Leia", []),
|
2023-03-17 01:14:41 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# 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.chatquality
|
|
|
|
def test_generate_search_query_using_question_and_answer_from_chat_history():
|
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("Does Luke Skywalker have any Siblings?", "Yes, Princess Leia", []),
|
2023-03-17 01:14:41 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# 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]
|
|
|
|
|
|
|
|
|
2021-11-26 12:53:03 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_chat_with_no_chat_history_or_retrieved_content():
|
2021-11-26 12:53:03 +01:00
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=[], # Assume no context retrieved from notes for the user_query
|
2023-03-14 18:27:58 +01:00
|
|
|
user_query="Hello, my name is Testatron. Who are you?",
|
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2021-11-26 12:53:03 +01:00
|
|
|
|
|
|
|
# Assert
|
2023-03-14 18:27:58 +01:00
|
|
|
expected_responses = ["Khoj", "khoj"]
|
2021-11-26 12:53:03 +01:00
|
|
|
assert len(response) > 0
|
2023-03-14 18:27:58 +01:00
|
|
|
assert any([expected_response in response for expected_response in expected_responses]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"Expected assistants name, [K|k]hoj, in response but got: " + response
|
2023-03-14 18:27:58 +01:00
|
|
|
)
|
2021-11-26 12:53:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_answer_from_chat_history_and_no_content():
|
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("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.", []),
|
2023-03-15 00:44:16 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=[], # Assume no context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="What is my name?",
|
2023-03-16 21:49:35 +01:00
|
|
|
conversation_log=populate_chat_history(message_list),
|
2023-03-15 00:44:16 +01:00
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
expected_responses = ["Testatron", "testatron"]
|
|
|
|
assert len(response) > 0
|
|
|
|
assert any([expected_response in response for expected_response in expected_responses]), (
|
2023-03-17 01:14:41 +01:00
|
|
|
"Expected [T|t]estatron in response but got: " + response
|
2023-03-15 00:44:16 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_answer_from_chat_history_and_previously_retrieved_content():
|
2023-03-15 21:18:08 +01:00
|
|
|
"Chat actor needs to use context in previous notes and chat history to answer question"
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("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."],
|
|
|
|
),
|
2023-03-15 00:44:16 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=[], # Assume no context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="Where was I born?",
|
2023-03-16 21:49:35 +01:00
|
|
|
conversation_log=populate_chat_history(message_list),
|
2023-03-15 00:44:16 +01:00
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert len(response) > 0
|
|
|
|
# Infer who I am and use that to infer I was born in Testville using chat history and previously retrieved notes
|
|
|
|
assert "Testville" in response
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_answer_from_chat_history_and_currently_retrieved_content():
|
2023-03-15 21:18:08 +01:00
|
|
|
"Chat actor needs to use context across currently retrieved notes and chat history to answer question"
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("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.", []),
|
2023-03-15 00:44:16 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=[
|
|
|
|
"Testatron was born on 1st April 1984 in Testville."
|
|
|
|
], # Assume context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="Where was I born?",
|
2023-03-16 21:49:35 +01:00
|
|
|
conversation_log=populate_chat_history(message_list),
|
2023-03-15 00:44:16 +01:00
|
|
|
api_key=api_key,
|
2023-03-14 18:27:58 +01:00
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2021-11-26 12:53:03 +01:00
|
|
|
|
2023-03-15 00:44:16 +01:00
|
|
|
# Assert
|
|
|
|
assert len(response) > 0
|
|
|
|
assert "Testville" in response
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-05-12 12:35:14 +02:00
|
|
|
def test_refuse_answering_unanswerable_question():
|
|
|
|
"Chat actor should not try make up answers to unanswerable questions."
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("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.", []),
|
2023-03-15 00:44:16 +01:00
|
|
|
]
|
|
|
|
|
2022-01-12 16:36:01 +01:00
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=[], # Assume no context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="Where was I born?",
|
2023-03-16 21:49:35 +01:00
|
|
|
conversation_log=populate_chat_history(message_list),
|
2023-02-17 17:04:26 +01:00
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2021-11-26 12:53:03 +01:00
|
|
|
|
|
|
|
# Assert
|
2023-05-12 12:35:14 +02:00
|
|
|
expected_responses = [
|
|
|
|
"don't know",
|
|
|
|
"do not know",
|
|
|
|
"no information",
|
|
|
|
"do not have",
|
|
|
|
"don't have",
|
|
|
|
"cannot answer",
|
|
|
|
"I'm sorry",
|
|
|
|
]
|
2021-11-26 12:53:03 +01:00
|
|
|
assert len(response) > 0
|
2023-03-15 21:18:08 +01:00
|
|
|
assert any([expected_response in response for expected_response in expected_responses]), (
|
|
|
|
"Expected chat actor to say they don't know in response, but got: " + response
|
|
|
|
)
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_answer_requires_current_date_awareness():
|
2023-03-15 21:18:08 +01:00
|
|
|
"Chat actor should be able to answer questions relative to current date using provided notes"
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
2023-03-24 15:55:22 +01:00
|
|
|
context = [
|
|
|
|
f"""{datetime.now().strftime("%Y-%m-%d")} "Naco Taco" "Tacos for Dinner"
|
|
|
|
Expenses:Food:Dining 10.00 USD""",
|
|
|
|
f"""{datetime.now().strftime("%Y-%m-%d")} "Sagar Ratna" "Dosa for Lunch"
|
|
|
|
Expenses:Food:Dining 10.00 USD""",
|
|
|
|
f"""2020-04-01 "SuperMercado" "Bananas"
|
|
|
|
Expenses:Food:Groceries 10.00 USD""",
|
|
|
|
f"""2020-01-01 "Naco Taco" "Burittos for Dinner"
|
|
|
|
Expenses:Food:Dining 10.00 USD""",
|
|
|
|
]
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=context, # Assume context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="What did I have for Dinner today?",
|
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
expected_responses = ["tacos", "Tacos"]
|
|
|
|
assert len(response) > 0
|
|
|
|
assert any([expected_response in response for expected_response in expected_responses]), (
|
|
|
|
"Expected [T|t]acos in response, but got: " + response
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_answer_requires_date_aware_aggregation_across_provided_notes():
|
2023-03-15 21:18:08 +01:00
|
|
|
"Chat actor should be able to answer questions that require date aware aggregation across multiple notes"
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
2023-03-24 15:55:22 +01:00
|
|
|
context = [
|
|
|
|
f"""# {datetime.now().strftime("%Y-%m-%d")} "Naco Taco" "Tacos for Dinner"
|
|
|
|
Expenses:Food:Dining 10.00 USD""",
|
|
|
|
f"""{datetime.now().strftime("%Y-%m-%d")} "Sagar Ratna" "Dosa for Lunch"
|
|
|
|
Expenses:Food:Dining 10.00 USD""",
|
|
|
|
f"""2020-04-01 "SuperMercado" "Bananas"
|
|
|
|
Expenses:Food:Groceries 10.00 USD""",
|
|
|
|
f"""2020-01-01 "Naco Taco" "Burittos for Dinner"
|
|
|
|
Expenses:Food:Dining 10.00 USD""",
|
|
|
|
]
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=context, # Assume context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="How much did I spend on dining this year?",
|
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert len(response) > 0
|
|
|
|
assert "20" in response
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_answer_general_question_not_in_chat_history_or_retrieved_content():
|
2023-03-15 21:18:08 +01:00
|
|
|
"Chat actor should be able to answer general questions not requiring looking at chat history or notes"
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
|
|
|
message_list = [
|
2023-03-24 15:55:22 +01:00
|
|
|
("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.", []),
|
2023-03-15 00:44:16 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=[], # Assume no context retrieved from notes for the user_query
|
2024-03-13 14:16:26 +01:00
|
|
|
user_query="Write a haiku about unit testing in 3 lines. Do not say anything else",
|
2023-03-16 21:49:35 +01:00
|
|
|
conversation_log=populate_chat_history(message_list),
|
2023-03-15 00:44:16 +01:00
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
expected_responses = ["test", "Test"]
|
|
|
|
assert len(response.splitlines()) == 3 # haikus are 3 lines long
|
|
|
|
assert any([expected_response in response for expected_response in expected_responses]), (
|
|
|
|
"Expected [T|t]est in response, but got: " + response
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
2023-03-15 21:18:08 +01:00
|
|
|
@pytest.mark.xfail(reason="Chat actor not consistently capable of asking for clarification yet.")
|
2023-03-15 01:56:14 +01:00
|
|
|
@pytest.mark.chatquality
|
2023-03-15 00:44:16 +01:00
|
|
|
def test_ask_for_clarification_if_not_enough_context_in_question():
|
2023-03-15 21:18:08 +01:00
|
|
|
"Chat actor should ask for clarification if question cannot be answered unambiguously with the provided context"
|
2023-03-15 00:44:16 +01:00
|
|
|
# Arrange
|
2023-03-24 15:55:22 +01:00
|
|
|
context = [
|
|
|
|
f"""# Ramya
|
|
|
|
My sister, Ramya, is married to Kali Devi. They have 2 kids, Ravi and Rani.""",
|
|
|
|
f"""# Fang
|
|
|
|
My sister, Fang Liu is married to Xi Li. They have 1 kid, Xiao Li.""",
|
|
|
|
f"""# Aiyla
|
|
|
|
My sister, Aiyla is married to Tolga. They have 3 kids, Yildiz, Ali and Ahmet.""",
|
|
|
|
]
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Act
|
2023-07-08 00:23:44 +02:00
|
|
|
response_gen = converse(
|
2023-03-24 15:55:22 +01:00
|
|
|
references=context, # Assume context retrieved from notes for the user_query
|
2023-03-15 00:44:16 +01:00
|
|
|
user_query="How many kids does my older sister have?",
|
|
|
|
api_key=api_key,
|
|
|
|
)
|
2023-07-08 00:23:44 +02:00
|
|
|
response = "".join([response_chunk for response_chunk in response_gen])
|
2023-03-15 00:44:16 +01:00
|
|
|
|
|
|
|
# Assert
|
|
|
|
expected_responses = ["which sister", "Which sister", "which of your sister", "Which of your sister"]
|
|
|
|
assert any([expected_response in response for expected_response in expected_responses]), (
|
2023-03-15 21:18:08 +01:00
|
|
|
"Expected chat actor to ask for clarification in response, but got: " + response
|
2023-03-15 00:44:16 +01:00
|
|
|
)
|
2023-03-16 21:49:35 +01:00
|
|
|
|
|
|
|
|
2024-03-23 17:39:38 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.chatquality
|
|
|
|
def test_agent_prompt_should_be_used(openai_agent):
|
|
|
|
"Chat actor should ask be tuned to think like an accountant based on the agent definition"
|
|
|
|
# Arrange
|
|
|
|
context = [
|
|
|
|
f"""I went to the store and bought some bananas for 2.20""",
|
|
|
|
f"""I went to the store and bought some apples for 1.30""",
|
|
|
|
f"""I went to the store and bought some oranges for 6.00""",
|
|
|
|
]
|
|
|
|
expected_responses = ["9.50", "9.5"]
|
|
|
|
|
|
|
|
# Act
|
|
|
|
response_gen = converse(
|
|
|
|
references=context, # Assume context retrieved from notes for the user_query
|
|
|
|
user_query="What did I buy?",
|
|
|
|
api_key=api_key,
|
|
|
|
)
|
|
|
|
no_agent_response = "".join([response_chunk for response_chunk in response_gen])
|
|
|
|
response_gen = converse(
|
|
|
|
references=context, # Assume context retrieved from notes for the user_query
|
|
|
|
user_query="What did I buy?",
|
|
|
|
api_key=api_key,
|
|
|
|
agent=openai_agent,
|
|
|
|
)
|
|
|
|
agent_response = "".join([response_chunk for response_chunk in response_gen])
|
|
|
|
|
|
|
|
# Assert that the model without the agent prompt does not include the summary of purchases
|
|
|
|
assert all([expected_response not in no_agent_response for expected_response in expected_responses]), (
|
|
|
|
"Expected chat actor to summarize values of purchases" + no_agent_response
|
|
|
|
)
|
|
|
|
assert any([expected_response in agent_response for expected_response in expected_responses]), (
|
|
|
|
"Expected chat actor to summarize values of purchases" + agent_response
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-03-13 12:19:13 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
@freeze_time("2024-04-04", ignore=["transformers"])
|
|
|
|
async def test_websearch_with_operators(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "Share popular posts on r/worldnews this month"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
responses = await generate_online_subqueries(user_query, {}, None)
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert any(
|
|
|
|
["reddit.com/r/worldnews" in response for response in responses]
|
|
|
|
), "Expected a search query to include site:reddit.com but got: " + str(responses)
|
|
|
|
|
|
|
|
assert any(
|
|
|
|
["site:reddit.com" in response for response in responses]
|
|
|
|
), "Expected a search query to include site:reddit.com but got: " + str(responses)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_websearch_khoj_website_for_info_about_khoj(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "Do you support image search?"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
responses = await generate_online_subqueries(user_query, {}, None)
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert any(
|
|
|
|
["site:khoj.dev" in response for response in responses]
|
|
|
|
), "Expected search query to include site:khoj.dev but got: " + str(responses)
|
|
|
|
|
|
|
|
|
2024-03-06 09:18:41 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_use_default_response_mode(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "What's the latest in the Israel/Palestine conflict?"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
mode = await aget_relevant_output_modes(user_query, {})
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert mode.value == "default"
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_use_image_response_mode(chat_client):
|
|
|
|
# Arrange
|
2024-03-13 14:16:26 +01:00
|
|
|
user_query = "Paint a scenery in Timbuktu in the winter"
|
2024-03-06 09:18:41 +01:00
|
|
|
|
|
|
|
# Act
|
|
|
|
mode = await aget_relevant_output_modes(user_query, {})
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert mode.value == "image"
|
|
|
|
|
|
|
|
|
2024-03-13 12:19:13 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_select_data_sources_actor_chooses_to_search_notes(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "Where did I learn to swim?"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
conversation_commands = await aget_relevant_information_sources(user_query, {})
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert ConversationCommand.Notes in conversation_commands
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_select_data_sources_actor_chooses_to_search_online(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "Where is the nearest hospital?"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
conversation_commands = await aget_relevant_information_sources(user_query, {})
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert ConversationCommand.Online in conversation_commands
|
|
|
|
|
|
|
|
|
Part 2: Add web UI updates for basic agent interactions (#675)
* Initial pass at backend changes to support agents
- Add a db model for Agents, attaching them to conversations
- When an agent is added to a conversation, override the system prompt to tweak the instructions
- Agents can be configured with prompt modification, model specification, a profile picture, and other things
- Admin-configured models will not be editable by individual users
- Add unit tests to verify agent behavior. Unit tests demonstrate imperfect adherence to prompt specifications
* Customize default behaviors for conversations without agents or with default agents
* Add a new web client route for viewing all agents
* Use agent_id for getting correct agent
* Add web UI views for agents
- Add a page to view all agents
- Add slugs to manage agents
- Add a view to view single agent
- Display active agent when in chat window
- Fix post-login redirect issue
* Fix agent view
* Spruce up the 404 page and improve the overall layout for agents pages
* Create chat actor for directly reading webpages based on user message
- Add prompt for the read webpages chat actor to extract, infer
webpage links
- Make chat actor infer or extract webpage to read directly from user
message
- Rename previous read_webpage function to more narrow
read_webpage_at_url function
* Rename agents_page -> agent_page
* Fix unit test for adding the filename to the compiled markdown entry
* Fix layout of agent, agents pages
* Merge migrations
* Let the name, slug of the default agent be Khoj, khoj
* Fix chat-related unit tests
* Add webpage chat command for read web pages requested by user
Update auto chat command inference prompt to show example of when to
use webpage chat command (i.e when url is directly provided in link)
* Support webpage command in chat API
- Fallback to use webpage when SERPER not setup and online command was
attempted
- Do not stop responding if can't retrieve online results. Try to
respond without the online context
* Test select webpage as data source and extract web urls chat actors
* Tweak prompts to extract information from webpages, online results
- Show more of the truncated messages for debugging context
- Update Khoj personality prompt to encourage it to remember it's capabilities
* Rename extract_content online results field to webpages
* Parallelize simple webpage read and extractor
Similar to what is being done with search_online with olostep
* Pass multiple webpages with their urls in online results context
Previously even if MAX_WEBPAGES_TO_READ was > 1, only 1 extracted
content would ever be passed.
URL of the extracted webpage content wasn't passed to clients in
online results context. This limited them from being rendered
* Render webpage read in chat response references on Web, Desktop apps
* Time chat actor responses & chat api request start for perf analysis
* Increase the keep alive timeout in the main application for testing
* Do not pipe access/error logs to separate files. Flow to stdout/stderr
* [Temp] Reduce to 1 gunicorn worker
* Change prod docker image to use jammy, rather than nvidia base image
* Use Khoj icon when Khoj web is installed on iOS as a PWA
* Make slug required for agents
* Simplify calling logic and prevent agent access for unauthenticated users
* Standardize to use personality over tuning in agent nomenclature
* Make filtering logic more stringent for accessible agents and remove unused method:
* Format chat message query
---------
Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
2024-03-26 13:43:24 +01:00
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_select_data_sources_actor_chooses_to_read_webpage(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "Summarize the wikipedia page on the history of the internet"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
conversation_commands = await aget_relevant_information_sources(user_query, {})
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert ConversationCommand.Webpage in conversation_commands
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------
|
|
|
|
@pytest.mark.anyio
|
|
|
|
@pytest.mark.django_db(transaction=True)
|
|
|
|
async def test_infer_webpage_urls_actor_extracts_correct_links(chat_client):
|
|
|
|
# Arrange
|
|
|
|
user_query = "Summarize the wikipedia page on the history of the internet"
|
|
|
|
|
|
|
|
# Act
|
|
|
|
urls = await infer_webpage_urls(user_query, {}, None)
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
assert "https://en.wikipedia.org/wiki/History_of_the_Internet" in urls
|
|
|
|
|
|
|
|
|
2023-03-16 21:49:35 +01:00
|
|
|
# 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
|