diff --git a/.gitignore b/.gitignore index ce92495..9bebfee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea .DS_STORE -deployment/env \ No newline at end of file +deployment/env +kit-deployment \ No newline at end of file diff --git a/deployment/template-env/.tutor-assistant.env b/deployment/template-env/.tutor-assistant.env index 6282781..8753d76 100644 --- a/deployment/template-env/.tutor-assistant.env +++ b/deployment/template-env/.tutor-assistant.env @@ -1,3 +1,4 @@ DATA_DIR=/var/lib/tutor-assistant/data HOST=tutor-assistant -OPENAI_API_KEY=<...> \ No newline at end of file +OPENAI_API_KEY=<...> +OPENAI_ORGANIZATION=<...> \ No newline at end of file diff --git a/deployment/tutor-assistant-nginx-proxy.conf b/deployment/tutor-assistant-nginx-proxy.conf index 8d2044a..92f77db 100644 --- a/deployment/tutor-assistant-nginx-proxy.conf +++ b/deployment/tutor-assistant-nginx-proxy.conf @@ -8,6 +8,12 @@ http { location /api/ { proxy_pass http://tutor-assistant-app-service:8080/api/; proxy_read_timeout 300s; + + proxy_set_header Connection ''; + proxy_http_version 1.1; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; } location /auth/ { diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt index 888fff8..03909b7 100644 --- a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/ChatService.kt @@ -171,10 +171,14 @@ class ChatService( private fun getContextFromJson(json: String): MessageContext { val root = objectMapper.readTree(json) return MessageContext( - root.getOrNull("kwargs").getOrNull("metadata").getOrNull("source")?.asText(), - root.getOrNull("kwargs").getOrNull("metadata").getOrNull("page")?.asInt(), - root.getOrNull("kwargs").getOrNull("page_content")?.asText(), - root.getOrNull("kwargs").getOrNull("metadata").getOrNull("originalKey")?.asText() + tutorAssistantId = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("tutorAssistantId")?.asText(), + title = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("title")?.asText(), + originalKey = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("originalKey")?.asText(), + isCalendar = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("isCalendar")?.asBoolean(), + heading = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("heading")?.asText(), + page = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("page")?.asInt(), + content = root.getOrNull("kwargs").getOrNull("page_content")?.asText(), + score = root.getOrNull("kwargs").getOrNull("metadata").getOrNull("score")?.asDouble(), ) } diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt index 22e1f9d..702b072 100644 --- a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/chat/model/messages/MessageContext.kt @@ -5,9 +5,13 @@ import jakarta.persistence.Embeddable @Embeddable data class MessageContext( - val source: String?, + val tutorAssistantId: String?, + val title: String?, + val originalKey: String?, + val isCalendar: Boolean?, + val heading: String?, val page: Int?, @Column(columnDefinition = "text") val content: String?, - val originalKey: String? + val score: Double? ) diff --git a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt index 43aa96b..b80ae36 100644 --- a/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt +++ b/tutor-assistant-app-service/src/main/kotlin/de/niklaskerkhoff/tutorassistantappservice/modules/documents/applications/entities/FileDocumentDtos.kt @@ -7,12 +7,14 @@ data class FileDocumentDto( val title: String, val loaderType: String, val collection: String?, + val fileStoreId: UUID ) { constructor(fileDocument: FileDocument) : this( id = fileDocument.id, title = fileDocument.title, loaderType = fileDocument.loaderType, - collection = fileDocument.collection + collection = fileDocument.collection, + fileStoreId = fileDocument.fileStoreId ) } diff --git a/tutor-assistant-app-service/src/main/resources/application.yml b/tutor-assistant-app-service/src/main/resources/application.yml index 396b468..36d2956 100644 --- a/tutor-assistant-app-service/src/main/resources/application.yml +++ b/tutor-assistant-app-service/src/main/resources/application.yml @@ -6,6 +6,11 @@ spring: application: name: tutor-assistant-app-service + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + datasource: username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD} diff --git a/tutor-assistant/resources/prompt_templates/base_template.txt b/tutor-assistant/resources/prompt_templates/base_template.txt index ad5b1cd..1424310 100644 --- a/tutor-assistant/resources/prompt_templates/base_template.txt +++ b/tutor-assistant/resources/prompt_templates/base_template.txt @@ -1,10 +1,14 @@ Du bist ein hilfreicher Assistent. Hier ein paar allgemeine Informationen, die zum Beantworten der Fragen des Benutzers relevant sein könnten: -- Wir haben heute folgendes Datum: 06.11.2024 +- Wir haben heute folgendes Datum: {date} - Die Benutzer sind studentische Tutoren für eine Programmieren-Vorlesung. Sie bringen anderen Studenten programmieren bei. - Es geht um die Programmiersprache Java. - Wenn nicht anders gefordert, schreibe angefragten Code in Java. Wenn nicht anders gefragt, schreibe deine Antworten auf {language}. - Schreibe Code immer auf Englisch. Kommentare im Code dürfen auf der Standardsprache sein. - Wenn du den Eindruck hast, dass eine Frage völlig am Thema vorbeigeht, beantworte sie dennoch so gut es geht, weise aber darauf hin, dass du dafür nicht entwickelt wurdest. - Sprich den Benutzer mit "Du" an + +Benutzer Markdown, um deine Antworten zu strukturieren. Markiere insbesondere wichtige Phrasen, damit der Benutzer auf den ersten Blick das Ergebnis sieht. +Wenn der Benutzer nach bestimmten Daten fragt, markiere diese fett. +Wenn du die Antwort nicht weißt, markiere die Stelle fett, an der du das schreibst. diff --git a/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt b/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt index f74b51d..5bf219c 100644 --- a/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt +++ b/tutor-assistant/resources/prompt_templates/calendar_vector_store.txt @@ -1 +1 @@ -Wichtige Termine und Fristen mit Zeit und Ort: Übungsblätter, Prüfungen, Veranstaltungen +Termine \ No newline at end of file diff --git a/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt index 1631f6f..e69de29 100644 --- a/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt +++ b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries.txt @@ -1,5 +0,0 @@ -Dies ist ein Chatverlauf. -Untersuche, welche groben Themen in der letzten Nachricht des Benutzers vorkommen. Benenne sie jeweils mit einem Begriff. -Gib möglichst wenig Begriffe aus. Sie sollen nur die Hauptthemen der Anfragen abdecken. Häufig wird nur ein Begriff notwendig sein. -Verwende die anderen Nachrichten nur, um den Kontext der letzten Nachricht zu verstehen. -Ganz wichtig: Beantworte nicht die Frage. Gib nur die Begriffe zu den groben Themen aus. Trenne sie mit Semikolons. diff --git a/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_1.txt b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_1.txt new file mode 100644 index 0000000..1631f6f --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_1.txt @@ -0,0 +1,5 @@ +Dies ist ein Chatverlauf. +Untersuche, welche groben Themen in der letzten Nachricht des Benutzers vorkommen. Benenne sie jeweils mit einem Begriff. +Gib möglichst wenig Begriffe aus. Sie sollen nur die Hauptthemen der Anfragen abdecken. Häufig wird nur ein Begriff notwendig sein. +Verwende die anderen Nachrichten nur, um den Kontext der letzten Nachricht zu verstehen. +Ganz wichtig: Beantworte nicht die Frage. Gib nur die Begriffe zu den groben Themen aus. Trenne sie mit Semikolons. diff --git a/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_2.txt b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_2.txt new file mode 100644 index 0000000..a66b07c --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_2.txt @@ -0,0 +1,5 @@ +Dies ist ein Chatverlauf. +Ich muss Anfragen an einen Vectorstore machen, sodass die richtigen Dokumente abgerufen werden, sodass die letzte Frage des Benutzers beantwortet werden kann. +Gib mir Anfragen, die ich stellen soll. Trenne die Anfragen mit einem Semikolon. +Gib mir möglichst wenig Anfragen, um Zeit zu sparen. Die Anfragen sollen dennoch alles Wichtige abdecken. In vielen Fällen wird eine Anfrage reichen. +Ganz wichtig: beantworte nicht die Frage, sondern gib nur die Anfragen aus. diff --git a/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_3.txt b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_3.txt new file mode 100644 index 0000000..37b52d6 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/hybrid_retriever/multiple_retriever_queries_3.txt @@ -0,0 +1,21 @@ +Dies ist ein Chatverlauf. Es soll eine Antwort auf die letzte Nachricht des Benutzers erstellt werden. +Um eine Antwort zu generieren, müssen erstmal die notwendigen Dokumente aus einem Vectorstore abgerufen werden. + +Ich möchte, dass du mir Anfragen ausgibst, die ich an den Vectorstore senden kann. Beachte dabei folgendes: +- Die Anfragen sollen beschreiben, was der Benutzer möchte. Zudem sollen sie so formuliert sein, dass bei einer Ähnlichkeitssuche die richtigen Dokumente übereinstimmen. +- Überlege dir, welche Informationen du bräuchtest und formuliere dahingehend Anfragen. +- Es soll die letzte Nachricht des Benutzers beantwortet werden. Beziehe jedoch den Chat-Verlauf mit ein, wenn es für den Kontext wichtig ist. +- Es sollen möglichst wenig Anfragen generiert werden. Je mehr Anfragen, desto länger muss der Benutzer warten. +- Die Anfragen sollen sehr verschieden sein. Bei ähnlichen Anfragen kämen dieselben Dokumente zurück, damit gäbe es Redundanz. +- Verwende unter keinen Umständen dieselben Begriffe in mehreren Anfragen. +- Die Anfragen sollen Dokumente identifizieren, nicht die Frage beantworten. +- Häufig wird nur eine Anfrage benötigt. +- Formuliere nur kurze Anfragen. Sie sollen nur wenige Wörter lang sein, wenn überhaupt mehr als ein Wort. +- Versuche wirklich den Kern der Frage des Benutzers zu erfassen und entsprechende Anfragen zu generieren. Gib das Thema aus und nicht, was die Frage dazu ist. +- Benutzer die Begriffe des Benutzers. +- Trenne die Anfragen mit einem Semikolon + +Es ist wirklich super wichtig, dass du nur ganz wenige Anfragen ausgibst. Gib wirklich nur dann mehrere aus, wenn es absolut notwendig ist, um den Kontext der Nachricht zu erfassen. Meistens ist dies nicht der Fall!!! +Verwende auf gar keinen Fall dasselbe Wort in mehreren Anfragen. Das bedeutet, dass man nur eine Anfrage braucht!!! + +Ganz wichtig: beantworte nicht die Frage, sondern gib nur die Anfragen aus. diff --git a/tutor-assistant/resources/prompt_templates/multi_steps/first.txt b/tutor-assistant/resources/prompt_templates/multi_steps/first.txt new file mode 100644 index 0000000..83890cf --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/multi_steps/first.txt @@ -0,0 +1,26 @@ +Dies ist ein Chat-Verlauf. Es soll eine Antwort auf die letzte Nachricht des Benutzers erstellt werden. +Dazu findest du im Folgenden einen Kontext. Es gibt zwei Möglichkeiten: + +1. Du kannst die Nachricht des Benutzers mit Sicherheit richtig beantworten: +Wenn du die Nachricht des Benutzers richtig beantworten kannst, dann gib die Antwort aus. +Bevor du deine Antwort gibst, erkläre genau, warum du diese Antwort gibst! +Nutze Markdown, um deine Antworten übersichtlich zu gestalten. Markiere insbesondere das finale Ergebnis fett. +Starte die Ausgabe der Antwort mit !!!RESPONSE!!! + +2. Du kannst die Nachricht nicht oder nicht sicher beantworten: +Gib keine Antwort auf die Nachricht aus! Stattdessen gib Phrasen aus, die ich an meinen Vectorstore stellen kann. Ich suche damit nach einem anderen Kontext und frage dich später damit nochmal. Du sollst also Anfragen ausgeben, die ich an den Vectorstore senden kann. Beachte dabei folgendes: +- Die Anfragen sollen beschreiben, was der Benutzer möchte. Zudem sollen sie so formuliert sein, dass bei einer Ähnlichkeitssuche die richtigen Dokumente übereinstimmen. +- Überlege dir, welche Informationen du bräuchtest und formuliere dahingehend Anfragen. +- Es soll die letzte Nachricht des Benutzers beantwortet werden. Beziehe jedoch den Chat-Verlauf mit ein, wenn es für den Kontext wichtig ist. +- Es sollen möglichst wenig Anfragen generiert werden. Je mehr Anfragen, desto länger muss der Benutzer warten. +- Die Anfragen sollen sehr verschieden sein. Bei ähnlichen Anfragen kämen dieselben Dokumente zurück, damit gäbe es Redundanz. +- Häufig wird nur eine Anfrage benötigt. +- Formuliere nur kurze Anfragen. Sie sollen nur wenige Wörter lang sein, wenn überhaupt mehr als ein Wort. +- Versuche wirklich den Kern der Frage des Benutzers zu erfassen und entsprechende Anfragen zu generieren. Gib das Thema aus und nicht, was die Frage dazu ist. +- Benutze die Begriffe des Benutzers. +- Trenne die Anfragen mit einem Semikolon +- Starte die Ausgabe der Anfragen mit !!!QUERIES!!! +- Ganz wichtig: beantworte nicht die Frage, sondern gib nur die Anfragen aus. + +Der Kontext: +{context} diff --git a/tutor-assistant/resources/prompt_templates/multi_steps/last.txt b/tutor-assistant/resources/prompt_templates/multi_steps/last.txt new file mode 100644 index 0000000..e425171 --- /dev/null +++ b/tutor-assistant/resources/prompt_templates/multi_steps/last.txt @@ -0,0 +1,14 @@ +Wenn du dir bei einer Antwort unsicher bist, teile sie mit, aber schreibe dazu, dass du dir unsicher bist. +Wenn du eine Antwort gar nicht weißt, teile dies dem Benutzer mit statt irgendetwas Falsches zu antworten. + +Verwende folgenden Kontext und die bisherige Konversation, um eine Antwort zu generieren, die dem Benutzer bestmöglich hilft. + +Bevor du deine Antwort gibst, erkläre genau, warum du diese Antwort gibst! + +Nutze Markdown, um deine Antworten übersichtlich zu gestalten. Markiere insbesondere das finale Ergebnis fett. +Wenn du dir nicht sicher bist oder die Antwort nicht weißt, markiere auch das fett. + +Ganz wichtig: Starte die Ausgabe deiner Antwort mit !!!RESPONSE!!! + +Der Kontext: +{context} diff --git a/tutor-assistant/tutor_assistant/controller/api/chats_controller.py b/tutor-assistant/tutor_assistant/controller/api/chats_controller.py index 82e1d24..1ab407f 100644 --- a/tutor-assistant/tutor_assistant/controller/api/chats_controller.py +++ b/tutor-assistant/tutor_assistant/controller/api/chats_controller.py @@ -6,8 +6,9 @@ from tutor_assistant.controller.config.domain_config import config from tutor_assistant.controller.utils.api_utils import check_request_body from tutor_assistant.controller.utils.data_transfer_utils import json_output -from tutor_assistant.controller.utils.langchain_utils import stream_chain +from tutor_assistant.controller.utils.langchain_utils import stream_chain, stream_response from tutor_assistant.domain.chats.message_chain_service import MessageChainService +from tutor_assistant.domain.chats.message_multi_steps_chain_service import MessageMultiStepsChainService from tutor_assistant.domain.chats.summary_chain_service import SummaryChainService from tutor_assistant.utils.string_utils import shorten_middle @@ -23,12 +24,14 @@ async def _message(request: Request): config.logger.info(f'POST /chats/message: len(message):{len(user_message_content)};len(history):{len(history)}') - chain = MessageChainService(config).create(user_message_content, history) + # chain = MessageChainService(config).create(user_message_content, history) + + response = MessageMultiStepsChainService(config).load_response(user_message_content, history) config.logger.info('Starting event-stream') return StreamingResponse( - stream_chain(chain), media_type="text/event-stream" + stream_response(response), media_type="text/event-stream" ) diff --git a/tutor-assistant/tutor_assistant/controller/api/demo_controller.py b/tutor-assistant/tutor_assistant/controller/api/demo_controller.py index abd6d05..527db8e 100644 --- a/tutor-assistant/tutor_assistant/controller/api/demo_controller.py +++ b/tutor-assistant/tutor_assistant/controller/api/demo_controller.py @@ -5,10 +5,36 @@ from langchain_core.prompts import ChatPromptTemplate from tutor_assistant.controller.config.domain_config import config +from tutor_assistant.domain.chats.message_multi_steps_chain_service import MessageMultiStepsChainService router = APIRouter() +@router.post('/demo/multi-steps') +async def _get_multi_steps(): + # user_message_content = 'Was mache ich in Artemis bei Exited Prematurely' + # history = [] + # MessageMultiStepsChainService(config).load_response(user_message_content, history) + + # user_message_content = 'Welches Datum haben wir heute?' + # history = [] + # MessageMultiStepsChainService(config).create(user_message_content, history) + # + # user_message_content = 'Wie viele Übungsblätter gibt es?' + # history = [] + # MessageMultiStepsChainService(config).create(user_message_content, history) + # + user_message_content = 'Was mache ich bei Exited Prematurely' + history = [] + response = MessageMultiStepsChainService(config).load_response(user_message_content, history) + + for item in response: + if 'context' in item: + print(item['context']) + if 'answer' in item: + print(item['answer']) + + @router.get("/demo/messages") async def _get_demo_messages(): template = ChatPromptTemplate.from_messages( @@ -20,7 +46,6 @@ async def _get_demo_messages(): ) - @router.post('/demo/meta-docs') async def _meta_docs(): documents = [ diff --git a/tutor-assistant/tutor_assistant/controller/api/documents_controller.py b/tutor-assistant/tutor_assistant/controller/api/documents_controller.py index 4cc380d..7d4db09 100644 --- a/tutor-assistant/tutor_assistant/controller/api/documents_controller.py +++ b/tutor-assistant/tutor_assistant/controller/api/documents_controller.py @@ -24,7 +24,7 @@ async def _add_document(request: Request): loader = get_loader(loader_creators, title, loader_type, loader_params) - ids = DocumentService(config).add(loader, original_key, is_calendar) + ids = DocumentService(config).add(loader, title, original_key, is_calendar) config.logger.info(f'Result: {ids}') diff --git a/tutor-assistant/tutor_assistant/controller/config/_logging_config.py b/tutor-assistant/tutor_assistant/controller/config/_logging_config.py index 2891145..9175d86 100644 --- a/tutor-assistant/tutor_assistant/controller/config/_logging_config.py +++ b/tutor-assistant/tutor_assistant/controller/config/_logging_config.py @@ -7,7 +7,7 @@ def get_logger() -> logging.Logger: logger = logging.getLogger('tutor-assistant') logger.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') console_handler = logging.StreamHandler() console_handler.setLevel(logging.DEBUG) diff --git a/tutor-assistant/tutor_assistant/controller/config/domain_config.py b/tutor-assistant/tutor_assistant/controller/config/domain_config.py index ac3be29..c33cb33 100644 --- a/tutor-assistant/tutor_assistant/controller/config/domain_config.py +++ b/tutor-assistant/tutor_assistant/controller/config/domain_config.py @@ -3,21 +3,26 @@ from langchain_openai import ChatOpenAI, OpenAIEmbeddings from tutor_assistant.controller.config._logging_config import get_logger -from tutor_assistant.controller.utils.model_utils import get_remote_ollama_chat_model from tutor_assistant.controller.utils.resource_utils import load_resources from tutor_assistant.domain.domain_config import DomainConfig from tutor_assistant.domain.vector_stores.chroma_repo import ChromaRepo -from tutor_assistant.domain.vector_stores.faiss_repo import FaissRepo + +_use_base_retriever = True _embeddings = OpenAIEmbeddings(model='text-embedding-3-large') +_store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_ws2324_with_meta_docs_index", _embeddings) + +if _use_base_retriever: + _store = ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_ws2324_no_meta_docs_index", _embeddings) + config = DomainConfig( ChatOpenAI(model='gpt-4o', temperature=0), # get_remote_ollama_chat_model('llama3.1:8b'), _embeddings, - # FaissRepo(f"{os.getenv('DATA_DIR')}/faiss_index", _embeddings), - ChromaRepo(f"{os.getenv('DATA_DIR')}/chroma_index", _embeddings), + _store, load_resources(f'{os.getcwd()}/resources'), get_logger(), - "Deutsch" + "Deutsch", + _use_base_retriever ) diff --git a/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py b/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py index cfe0265..1f12cc2 100644 --- a/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py +++ b/tutor-assistant/tutor_assistant/controller/utils/langchain_utils.py @@ -1,4 +1,5 @@ import json +from typing import Iterator from langchain_core.documents import Document from langchain_core.runnables import Runnable @@ -7,7 +8,11 @@ def stream_chain(chain: Runnable, answer_key='answer', context_key='context'): - for item in chain.stream({}): + yield from stream_response(chain.stream({}), answer_key, context_key) + + +def stream_response(response: Iterator, answer_key='answer', context_key='context'): + for item in response: if context_key in item: yield from _handle_context(item[context_key], context_key) elif answer_key in item: diff --git a/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py b/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py index 8531240..656f479 100644 --- a/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py +++ b/tutor-assistant/tutor_assistant/domain/chats/message_chain_service.py @@ -8,7 +8,7 @@ from tutor_assistant.controller.utils.data_transfer_utils import messages_from_history from tutor_assistant.controller.utils.langchain_utils import escape_prompt -from tutor_assistant.domain.documents.retrievers.hybrid_retriever import HybridRetriever +from tutor_assistant.domain.documents.retrievers.queries_loader_retriever import QueriesLoaderRetriever from tutor_assistant.domain.domain_config import DomainConfig from tutor_assistant.domain.utils.templates import prepend_base_template @@ -23,12 +23,16 @@ def create(self, user_message_content: str, history: list[dict[str, str]]) -> Ru chat_prompt = self._get_chat_prompt(messages) model_chain = self._get_model_chain(chat_prompt) - retriever = HybridRetriever(self._config) + retriever = QueriesLoaderRetriever(self._config) + - # retriever_chain = (lambda _: user_message_content) | self._get_self_query_retriever_chain(user_message_content) - # retriever_chain = (lambda _: user_message_content) | self._get_base_retriever_chain(user_message_content) retriever_chain = (lambda _: messages) | retriever + if self._config.use_base_retriever: + retriever_chain = (lambda _: self._search_with_score(user_message_content)) + # retriever_chain = (lambda _: user_message_content) | self._get_self_query_retriever_chain(user_message_content) + + return ( RunnablePassthrough .assign(context=retriever_chain) @@ -79,3 +83,25 @@ def _get_self_query_retriever_chain(self, query: str) -> RunnableSerializable[An print(retriever.invoke(query)) return (lambda _: query) | retriever + + def _search_with_score(self, query: str) -> list[Document]: + self._config.logger.info(f'Base Retriever: Running for "{query}') + try: + docs, scores = zip( + *self._config.vector_store_manager.load().similarity_search_with_score( + query, + k=4 + ) + ) + except Exception as e: + print('Exception:', e) + return [] + result = [] + doc: Document + for doc, np_score in zip(docs, scores): + score = float(np_score) + doc.metadata['score'] = score + if np_score < 2: + result.append(doc) + + return result diff --git a/tutor-assistant/tutor_assistant/domain/chats/message_multi_steps_chain_service.py b/tutor-assistant/tutor_assistant/domain/chats/message_multi_steps_chain_service.py new file mode 100644 index 0000000..4ac5ceb --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/chats/message_multi_steps_chain_service.py @@ -0,0 +1,101 @@ +from typing import Iterator, Generator, Any + +from langchain_core.documents import Document +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnablePassthrough, RunnableSerializable + +from tutor_assistant.controller.utils.data_transfer_utils import messages_from_history +from tutor_assistant.controller.utils.langchain_utils import escape_prompt +from tutor_assistant.domain.documents.retrievers.with_references_retriever import WithReferencesRetriever +from tutor_assistant.domain.domain_config import DomainConfig +from tutor_assistant.domain.utils.templates import prepend_base_template + + +class MessageMultiStepsChainService: + def __init__(self, config: DomainConfig): + self._config = config + + def load_response(self, user_message_content: str, history: list[dict[str, str]]) -> Generator[Any, Any, None]: + messages = self._get_all_messages(user_message_content, history) + yield from self._get_response_for_queries(messages, [user_message_content], ['first.txt', 'last.txt']) + + def _get_response_for_queries( + self, messages: list[tuple[str, str]], queries: list[str], templates: list[str] + ) -> Generator[Any, Any, None]: + + retriever_chain = self._get_retriever_chain(queries) + model_chain = self._get_model_chain(messages, templates[0]) + + result = RunnablePassthrough.assign(context=retriever_chain).assign(answer=model_chain).stream({}) + + contexts = [] + answer_start = '' + + for item in result: + if 'context' in item: + contexts.append(item) + elif 'answer' in item: + answer_start += item['answer'] + + if '!!!QUERIES!!!' in answer_start: + print('!!!QUERIES!!!') + queries = self._get_queries_from_answer(result) + yield from self._get_response_for_queries(messages, queries, templates[1:]) + break + elif '!!!RESPONSE!!!' in answer_start: + print('!!!RESPONSE!!!') + yield from contexts + yield from self._yield_response(result) + break + elif len(answer_start) > 14: + print('Long enough') + yield from contexts + yield answer_start + yield from self._yield_response(result) + break + + @staticmethod + def _get_all_messages(user_message_content: str, history) -> list[tuple[str, str]]: + messages = [] + for msg in messages_from_history(history): + messages.append(msg) + messages.append(('user', escape_prompt(user_message_content))) + + return messages + + def _get_queries_from_answer(self, result: Iterator) -> list[str]: + answer = '' + for item in result: + if 'answer' in item: + answer += item['answer'] + + queries = answer.split(';') + self._config.logger.info(f'Queries from chat model: {queries}') + return queries + + @staticmethod + def _yield_response(result: Iterator) -> Generator[Any, Any, None]: + for item in result: + yield item + + def _get_retriever_chain(self, queries: list[str]) -> RunnableSerializable[Any, list[Document]]: + return (lambda _: queries) | WithReferencesRetriever(self._config) + + def _get_model_chain(self, messages: list[tuple[str, str]], template: str): + prompt = self._get_chat_prompt(messages, template) + model = self._config.chat_model + parser = StrOutputParser() + + return prompt | model | parser + + def _get_chat_prompt(self, messages: list[tuple[str, str]], template: str) -> ChatPromptTemplate: + template = self._config.resources['prompt_templates']['multi_steps'][template] + complete_template = prepend_base_template(self._config, template) + + prompt_messages = [('system', complete_template)] + prompt_messages.extend(messages) + + prompt_template = ChatPromptTemplate.from_messages(prompt_messages) + + return prompt_template diff --git a/tutor-assistant/tutor_assistant/domain/documents/document_service.py b/tutor-assistant/tutor_assistant/domain/documents/document_service.py index d6f82fa..1c25d53 100644 --- a/tutor-assistant/tutor_assistant/domain/documents/document_service.py +++ b/tutor-assistant/tutor_assistant/domain/documents/document_service.py @@ -13,17 +13,18 @@ class DocumentService: def __init__(self, config: DomainConfig): self._config = config - def add(self, loader: BaseLoader, original_key: str, is_calendar: bool) -> list[str]: + def add(self, loader: BaseLoader, title: str, original_key: str, is_calendar: bool) -> list[str]: documents = loader.load() ids: list[str] = [] for i, doc in enumerate(documents): doc.id = str(uuid.uuid4()) doc.metadata['id'] = doc.id + doc.metadata['title'] = title doc.metadata['originalKey'] = original_key doc.metadata['isCalendar'] = is_calendar ids.append(doc.id) - meta_docs = self._handle_meta_docs(documents) + meta_docs = self._get_meta_docs(documents) store = self._config.vector_store_manager.load() store_ids = store.add_documents(documents) @@ -32,8 +33,9 @@ def add(self, loader: BaseLoader, original_key: str, is_calendar: bool) -> list[ raise RuntimeError( f'ids and store_ids should be equal, but got ids={ids} and store_ids={store_ids}') - meta_doc_ids = store.add_documents(meta_docs) - store_ids.extend(meta_doc_ids) + if len(meta_docs) > 0: + meta_doc_ids = store.add_documents(meta_docs) + store_ids.extend(meta_doc_ids) self._config.vector_store_manager.save(store) @@ -47,7 +49,7 @@ def delete(self, ids: list[str]) -> Optional[bool]: return success @staticmethod - def _handle_meta_docs(docs: list[Document]) -> list[Document]: + def _get_meta_docs(docs: list[Document]) -> list[Document]: meta_docs = [] for doc in docs: if 'headings' in doc.metadata: @@ -56,7 +58,6 @@ def _handle_meta_docs(docs: list[Document]) -> list[Document]: metadata={'references': doc.metadata['id']} ) meta_docs.append(meta_doc) - del doc.metadata['headings'] return meta_docs diff --git a/tutor-assistant/tutor_assistant/domain/documents/retrievers/queries_loader_retriever.py b/tutor-assistant/tutor_assistant/domain/documents/retrievers/queries_loader_retriever.py new file mode 100644 index 0000000..377d32a --- /dev/null +++ b/tutor-assistant/tutor_assistant/domain/documents/retrievers/queries_loader_retriever.py @@ -0,0 +1,38 @@ +from typing import Any + +from langchain_core.callbacks import CallbackManagerForRetrieverRun +from langchain_core.documents import Document +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.retrievers import BaseRetriever + +from tutor_assistant.domain.documents.retrievers.with_references_retriever import WithReferencesRetriever +from tutor_assistant.domain.domain_config import DomainConfig + + +class QueriesLoaderRetriever(BaseRetriever): + def __init__(self, config: DomainConfig, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self._chat_model = config.chat_model + self._vector_store = config.vector_store_manager.load() + self._config = config + + def _get_relevant_documents( + self, messages: list[tuple[str, str]], *, run_manager: CallbackManagerForRetrieverRun + ) -> list[Document]: + pass + # queries = self._get_queries(messages) + # return WithReferencesRetriever(self._config).search(queries) + + def _get_queries(self, messages: list[tuple[str, str]]) -> list[str]: + chain = self._get_chat_prompt(messages) | self._chat_model + content = chain.invoke({}).content + print('content', content) + queries = content.split(';') + + return queries + + def _get_chat_prompt(self, messages: list[tuple[str, str]]) -> ChatPromptTemplate: + multiple_prompts = self._config.resources['prompt_templates']['hybrid_retriever'][ + 'multiple_retriever_queries_3.txt'] + prompt_messages = messages + [('system', multiple_prompts)] + return ChatPromptTemplate.from_messages(prompt_messages) diff --git a/tutor-assistant/tutor_assistant/domain/documents/retrievers/hybrid_retriever.py b/tutor-assistant/tutor_assistant/domain/documents/retrievers/with_references_retriever.py similarity index 64% rename from tutor-assistant/tutor_assistant/domain/documents/retrievers/hybrid_retriever.py rename to tutor-assistant/tutor_assistant/domain/documents/retrievers/with_references_retriever.py index eed74cf..54f009a 100644 --- a/tutor-assistant/tutor_assistant/domain/documents/retrievers/hybrid_retriever.py +++ b/tutor-assistant/tutor_assistant/domain/documents/retrievers/with_references_retriever.py @@ -3,54 +3,47 @@ from langchain_core.callbacks import CallbackManagerForRetrieverRun from langchain_core.documents import Document -from langchain_core.prompts import ChatPromptTemplate from langchain_core.retrievers import BaseRetriever from tutor_assistant.domain.domain_config import DomainConfig from tutor_assistant.utils.list_utils import distinct_by -class HybridRetriever(BaseRetriever): +class WithReferencesRetriever(BaseRetriever): + def __init__(self, config: DomainConfig, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self._chat_model = config.chat_model + self._vector_store = config.vector_store_manager.load() - self._multiple_prompts = ( - config.resources)['prompt_templates']['hybrid_retriever']['multiple_retriever_queries.txt'] + self._logger = config.logger def _get_relevant_documents( - self, messages: list[tuple[str, str]], *, run_manager: CallbackManagerForRetrieverRun - ) -> list[Document]: - queries = self._get_queries(messages) + self, queries: list[str], *, run_manager: CallbackManagerForRetrieverRun) -> list[Document]: + docs: list[Document] = [] for query in queries: + query = query.strip() + self._logger.info(f'Searching for "{query}"') queried_docs = self._search_with_score(query.strip()) docs.extend(queried_docs) - referenced_docs = self._get_referenced_by(query, docs) + self._logger.info(f'Retrieved {len(queried_docs)} documents') + referenced_docs = self._get_referenced_docs(query, queried_docs) + self._logger.info(f'Retrieved {len(referenced_docs)} referenced documents') docs.extend(referenced_docs) + self._logger.info(f'Retrieved {len(docs)} documents in total') distinct = distinct_by(self._id_or_random, docs) - filtered = list(filter(lambda x: 'id' in x.metadata, distinct)) - return distinct - - def _get_queries(self, messages: list[tuple[str, str]]) -> list[str]: - chain = self._get_chat_prompt(messages) | self._chat_model - content = chain.invoke({}).content - print('content', content) - queries = content.split(';') - - return queries - - def _get_chat_prompt(self, messages: list[tuple[str, str]]) -> ChatPromptTemplate: - prompt_messages = messages + [('system', self._multiple_prompts)] - return ChatPromptTemplate.from_messages(prompt_messages) + self._logger.info(f'Retrieved {len(distinct)} distinct documents') + real_docs = list(filter(lambda x: 'id' in x.metadata, distinct)) + self._logger.info(f'Retrieved {len(real_docs)} real documents') + return real_docs def _search_with_score(self, query: str) -> list[Document]: try: docs, scores = zip( *self._vector_store.similarity_search_with_score( query, - k=5 + k=8 ) ) except Exception as e: @@ -61,12 +54,12 @@ def _search_with_score(self, query: str) -> list[Document]: for doc, np_score in zip(docs, scores): score = float(np_score) doc.metadata['score'] = score - if np_score < 5: + if np_score < 1.1: result.append(doc) return result - def _get_referenced_by(self, query: str, docs: list[Document]) -> list[Document]: + def _get_referenced_docs(self, query: str, docs: list[Document]) -> list[Document]: referenced_docs: list[Document] = [] for doc in docs: if 'references' in doc.metadata: @@ -81,6 +74,12 @@ def _get_referenced_by(self, query: str, docs: list[Document]) -> list[Document] lambda d: (d.metadata['id'] in ids) if 'id' in d.metadata else False, queried_docs )) + + for filtered_doc in filtered_docs: + if 'score' in filtered_doc.metadata: + print('has already a score') + filtered_doc.metadata['score'] = doc.metadata['score'] + referenced_docs.extend(filtered_docs) return referenced_docs diff --git a/tutor-assistant/tutor_assistant/domain/domain_config.py b/tutor-assistant/tutor_assistant/domain/domain_config.py index 0939314..46c446c 100644 --- a/tutor-assistant/tutor_assistant/domain/domain_config.py +++ b/tutor-assistant/tutor_assistant/domain/domain_config.py @@ -15,6 +15,7 @@ def __init__(self, resources: dict[str, Any], logger: logging.Logger, language: str, + use_base_retriever: bool = False, ): self.chat_model = chat_model self.embeddings = embeddings @@ -22,5 +23,7 @@ def __init__(self, self.resources = resources self.logger = logger self.language = language + self.use_base_retriever = use_base_retriever logger.info('Application configuration complete.') + logger.info(f"Using {'base' if use_base_retriever else 'hybrid'} retriever") diff --git a/tutor-assistent-web/index.html b/tutor-assistent-web/index.html index e4b78ea..1cf5323 100644 --- a/tutor-assistent-web/index.html +++ b/tutor-assistent-web/index.html @@ -4,7 +4,7 @@ -