From 9bf3b6deae742ca72591c7de0f3f176f1cf2ef17 Mon Sep 17 00:00:00 2001 From: Saisakul Chernbumroong <159020482+saisakul@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:12:02 +0000 Subject: [PATCH 1/5] Spike LLM to choose route (#113) * spike LLM to choose route * add explanation why choosing metadata prompt --------- Co-authored-by: Saisakul Chernbumroong --- notebooks/LLM_choose_route.ipynb | 402 +++++++++++++++++++++ redbox-core/redbox/chains/components.py | 21 +- redbox-core/redbox/retriever/__init__.py | 4 +- redbox-core/redbox/retriever/queries.py | 17 + redbox-core/redbox/retriever/retrievers.py | 45 ++- 5 files changed, 473 insertions(+), 16 deletions(-) create mode 100644 notebooks/LLM_choose_route.ipynb diff --git a/notebooks/LLM_choose_route.ipynb b/notebooks/LLM_choose_route.ipynb new file mode 100644 index 000000000..be8531833 --- /dev/null +++ b/notebooks/LLM_choose_route.ipynb @@ -0,0 +1,402 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext dotenv\n", + "%dotenv /Users/saisakulchernbumroong/Documents/vsprojects/DBT_redbox/redbox/.env.test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### We are investigating whether an LLM can identify correct tool e.g. search or summarise given user's question" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We explore prompts that 1) uses only user'question to determine the route, and 2) uses user's question and basic documents metadata (document name, description and keywords) to determine route. The reason is that by giving information about document metadata, LLM is more equipped with information to help determining the route." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Conclusion: Based on the experiment results, we achieved 80% using the prompt with metadata. In addition, using this prompt show that LLM uses slightly shorter time to make decision. Therefore, we will be implementing a new node with LLM_decide_tool_1 going forward." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prompts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "LLM_decide_tool_0 = (\"\"\"Given analysis request, determine whether to use search or summarise tools.\n", + "\n", + "Context:\n", + "- Search tool: Used to find and analyze specific relevant sections in a document\n", + "- Summarise tool: Used to create an overview of the entire document's content\n", + "\n", + "Please analyze the following request:\n", + "{question}\n", + "\n", + "Follow these steps to determine the appropriate tool:\n", + "\n", + "1. Identify the key requirements in the request:\n", + " - Is it asking for specific information or general overview?\n", + " - Are there specific topics/keywords mentioned?\n", + " - Is the scope focused or broad?\n", + "\n", + "2. Evaluate request characteristics:\n", + " - Does it need comprehensive coverage or targeted information?\n", + " - Are there specific questions to answer?\n", + " - Is context from the entire document needed?\n", + "\n", + "3. Recommend either search or summarise based on:\n", + " - If focused/specific information is needed → Recommend search\n", + " - If general overview/main points needed → Recommend summarise\n", + "\n", + "- Recommended Tool: [Search/Summarise]\n", + "- Reason for the recommendation\n", + "Provide your recommendation in this format:\n", + "\\n{format_instructions}\\n\n", + " \n", + "Analysis request:\n", + "{question}\n", + "\"\"\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "LLM_decide_tool_1 = (\"\"\"Given analysis request and document demtadata, determine whether to use search or summarise tools.\n", + "\n", + "Context:\n", + "- Search tool: Used to find and analyze specific relevant sections in a document\n", + "- Summarise tool: Used to create an overview of the entire document's content\n", + "\n", + "Please analyze the following request:\n", + "{question}\n", + "\n", + "Follow these steps to determine the appropriate tool:\n", + "\n", + "1. Identify the key requirements in the request:\n", + " - Is it asking for specific information or general overview?\n", + " - Are there specific topics/keywords mentioned?\n", + " - Is the scope focused or broad?\n", + "\n", + "2. Evaluate request characteristics:\n", + " - Does it need comprehensive coverage or targeted information?\n", + " - Are there specific questions to answer?\n", + " - Is context from the entire document needed?\n", + "\n", + "3. Recommend either search or summarise based on:\n", + " - If focused/specific information is needed → Recommend search\n", + " - If general overview/main points needed → Recommend summarise\n", + " - Priortise search tool if both tools can be used to produce good answer \n", + "\n", + "- Recommended Tool: [Search/Summarise]\n", + "\n", + "Provide your recommendation in this format:\n", + "\\n{format_instructions}\\n\n", + "\n", + "Analysis request:\n", + "{question}\n", + " \n", + "Document metadata: {metadata}\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating necceasry functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from enum import Enum\n", + "from redbox.chains.components import get_basic_metadata_retriever\n", + "import time\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from redbox.chains.parser import ClaudeParser\n", + "from pydantic import BaseModel\n", + "from redbox.chains.components import get_chat_llm\n", + "from redbox.models.chain import RedboxState, RedboxQuery, AISettings\n", + "from langchain_core.runnables import chain\n", + "from uuid import uuid4\n", + "from redbox.models.settings import ChatLLMBackend\n", + "from redbox.models.settings import get_settings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_state(user_uuid, prompts, documents, ai_setting):\n", + " q = RedboxQuery(\n", + " question=f\"{prompts[-1]}\",\n", + " s3_keys=documents,\n", + " user_uuid=user_uuid,\n", + " chat_history=prompts[:-1],\n", + " ai_settings=ai_setting,\n", + " permitted_s3_keys=documents,\n", + " )\n", + "\n", + " return RedboxState(\n", + " request=q,\n", + " )\n", + "\n", + "def basic_chat_chain(system_prompt, _additional_variables: dict = {}, parser=None):\n", + " @chain\n", + " def _basic_chat_chain(state: RedboxState):\n", + " nonlocal parser\n", + " llm = get_chat_llm(state.request.ai_settings.chat_backend)\n", + " context = ({\n", + " \"question\": state.request.question,\n", + " }\n", + " | _additional_variables\n", + " )\n", + " \n", + " if parser:\n", + " format_instructions = parser.get_format_instructions()\n", + " prompt = ChatPromptTemplate([(system_prompt)], partial_variables={\"format_instructions\": format_instructions})\n", + " else:\n", + " prompt = ChatPromptTemplate([(system_prompt)])\n", + " parser = ClaudeParser()\n", + " chain = prompt | llm | parser\n", + " return chain.invoke(context)\n", + " return _basic_chat_chain\n", + "\n", + "def lm_choose_route(system_prompt: str, parser: ClaudeParser):\n", + " metadata = None\n", + " \n", + " @chain\n", + " def get_metadata(state: RedboxState):\n", + " nonlocal metadata\n", + " env = get_settings()\n", + " retriever = get_basic_metadata_retriever(env)\n", + " metadata = retriever.invoke(state)\n", + " return state\n", + " \n", + " @chain\n", + " def use_result(state: RedboxState):\n", + " chain = basic_chat_chain(system_prompt=system_prompt, parser=parser, _additional_variables={'metadata': metadata})\n", + " return chain.invoke(state)\n", + " \n", + " return get_metadata | use_result\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining class to capture LLM response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "members = [\"Search\", \"Summarise\"]\n", + "\n", + "#create options map for the supervisor output parser.\n", + "tools_options = {tool:tool for tool in members}\n", + "\n", + "#create Enum object\n", + "ToolEnum = Enum('ToolEnum', tools_options)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class AgentDecision(BaseModel):\n", + " next: ToolEnum = ToolEnum.Search\n", + "\n", + "class AgentDecisionWithReason(BaseModel):\n", + " next: ToolEnum = ToolEnum.Search\n", + " reason: str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing approach" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set up Redbox state" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = get_state(user_uuid=uuid4(), prompts=['What did Serena Williams say about fairness in relation to her experience with postnatal complications?'], documents=['test@dbt.gov.uk/1 The power chapter.pdf'], ai_setting=AISettings(chat_backend=ChatLLMBackend(name=\"anthropic.claude-3-sonnet-20240229-v1:0\", provider=\"bedrock\")))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test prompt 0 without metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "prompt = LLM_decide_tool_0\n", + "agent_parser = ClaudeParser(pydantic_object=AgentDecisionWithReason)\n", + "# return results with reason\n", + "test_chain_reason = basic_chat_chain(system_prompt=prompt, parser=agent_parser)\n", + "start = time.time()\n", + "test_chain_reason.invoke(x)\n", + "print(f'time used: {time.time() - start}')\n", + "\n", + "# return results without reason\n", + "agent_parser = ClaudeParser(pydantic_object=AgentDecision)\n", + "test_chain_no_reason = basic_chat_chain(system_prompt=prompt, parser=agent_parser)\n", + "start = time.time()\n", + "test_chain_no_reason.invoke(x)\n", + "print(f'time used: {time.time() - start}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test prompt 1 with metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "prompt = LLM_decide_tool_1\n", + "agent_parser = ClaudeParser(pydantic_object=AgentDecisionWithReason)\n", + "# return results with reason\n", + "test_chain_reason = lm_choose_route(system_prompt=prompt, parser=agent_parser)\n", + "start = time.time()\n", + "test_chain_reason.invoke(x)\n", + "print(f'time used: {time.time() - start}')\n", + "\n", + "# return results without reason\n", + "agent_parser = ClaudeParser(pydantic_object=AgentDecision)\n", + "test_chain_no_reason = lm_choose_route(system_prompt=prompt, parser=agent_parser)\n", + "start = time.time()\n", + "test_chain_no_reason.invoke(x)\n", + "print(f'time used: {time.time() - start}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Experiments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "### Experiments\n", + "import pandas as pd\n", + "df = pd.read_csv(\"/Users/saisakulchernbumroong/Documents/RB exp/intent_exp/route_results_consensus.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# making change on the chain and correct parser you want to test here\n", + "agent_parser = ClaudeParser(pydantic_object=AgentDecisionWithReason)\n", + "test_chain = lm_choose_route(system_prompt=LLM_decide_tool_1, parser=agent_parser)\n", + "\n", + "\n", + "next_move = []\n", + "move_reason = []\n", + "for prompt in df.Prompt:\n", + " x = get_state(user_uuid=uuid4(), prompts=[prompt], documents=['test@dbt.gov.uk/1 The power chapter.pdf'], ai_setting=AISettings(chat_backend=ChatLLMBackend(name=\"anthropic.claude-3-sonnet-20240229-v1:0\", provider=\"bedrock\")))\n", + " res = test_chain.invoke(x)\n", + " next_move += [res.next.value]\n", + " move_reason += [res.reason] # comment this line out if you are not returning reason\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# save result\n", + "save_path = \"/Users/saisakulchernbumroong/Documents/RB exp/intent_exp/intent_prompt3_basic_metadata.csv\"\n", + "pd.DataFrame({'Prompt': df.Prompt, 'Consensus': df.consensus, 'LLM_tool_select': next_move, 'LLM_reason': move_reason}).to_csv(save_path)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "redbox-root-In7wI2Lt-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/redbox-core/redbox/chains/components.py b/redbox-core/redbox/chains/components.py index 74ad9d6e1..af49d3aee 100644 --- a/redbox-core/redbox/chains/components.py +++ b/redbox-core/redbox/chains/components.py @@ -1,32 +1,30 @@ import logging - import os from functools import cache from dotenv import load_dotenv +from langchain.chat_models import init_chat_model +from langchain_community.embeddings import BedrockEmbeddings from langchain_core.embeddings import Embeddings, FakeEmbeddings -from langchain_core.tools import StructuredTool from langchain_core.runnables import Runnable +from langchain_core.tools import StructuredTool from langchain_core.utils import convert_to_secret_str # from langchain_elasticsearch import ElasticsearchRetriever from langchain_openai.embeddings import AzureOpenAIEmbeddings, OpenAIEmbeddings - from redbox.chains.parser import StreamingJsonOutputParser +from redbox.models.chain import StructuredResponseWithCitations from redbox.models.settings import ChatLLMBackend, Settings from redbox.retriever import ( AllElasticsearchRetriever, - ParameterisedElasticsearchRetriever, + BasicMetadataRetriever, MetadataRetriever, OpenSearchRetriever, + ParameterisedElasticsearchRetriever, ) -from langchain_community.embeddings import BedrockEmbeddings -from langchain.chat_models import init_chat_model -from redbox.models.chain import StructuredResponseWithCitations from redbox.transform import bedrock_tokeniser - logger = logging.getLogger(__name__) load_dotenv() @@ -118,6 +116,13 @@ def get_metadata_retriever(env: Settings): ) +def get_basic_metadata_retriever(env: Settings): + return BasicMetadataRetriever( + es_client=env.elasticsearch_client(), + index_name=env.elastic_chunk_alias, + ) + + def get_structured_response_with_citations_parser() -> tuple[Runnable, str]: """ Returns the output parser (as a runnable) for creating the StructuredResponseWithCitations object diff --git a/redbox-core/redbox/retriever/__init__.py b/redbox-core/redbox/retriever/__init__.py index d7e10d215..6c3f0ea26 100644 --- a/redbox-core/redbox/retriever/__init__.py +++ b/redbox-core/redbox/retriever/__init__.py @@ -1,8 +1,9 @@ from .retrievers import ( AllElasticsearchRetriever, - ParameterisedElasticsearchRetriever, + BasicMetadataRetriever, MetadataRetriever, OpenSearchRetriever, + ParameterisedElasticsearchRetriever, ) __all__ = [ @@ -10,4 +11,5 @@ "AllElasticsearchRetriever", "MetadataRetriever", "OpenSearchRetriever", + "BasicMetadataRetriever", ] diff --git a/redbox-core/redbox/retriever/queries.py b/redbox-core/redbox/retriever/queries.py index 19a708169..68600965e 100644 --- a/redbox-core/redbox/retriever/queries.py +++ b/redbox-core/redbox/retriever/queries.py @@ -90,6 +90,23 @@ def get_metadata( } +def get_minimum_metadata( + chunk_resolution: ChunkResolution | None, + state: RedboxState, +) -> dict[str, Any]: + """Retrive document metadata without page_content""" + query_filter = build_query_filter( + selected_files=state.request.s3_keys, + permitted_files=state.request.permitted_s3_keys, + chunk_resolution=chunk_resolution, + ) + + return { + "_source": {"includes": ["metadata.name", "metadata.description", "metadata.keywords"]}, + "query": {"bool": {"must": {"match_all": {}}, "filter": query_filter}}, + } + + def build_document_query( query: str, query_vector: list[float], diff --git a/redbox-core/redbox/retriever/retrievers.py b/redbox-core/redbox/retriever/retrievers.py index 54ab2292a..5260d7010 100644 --- a/redbox-core/redbox/retriever/retrievers.py +++ b/redbox-core/redbox/retriever/retrievers.py @@ -1,23 +1,28 @@ import logging - +import os from functools import partial from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union, cast from elasticsearch import Elasticsearch - -# from elasticsearch.helpers import scan -from opensearchpy.helpers import scan -from opensearchpy import OpenSearch from kneed import KneeLocator from langchain_core.callbacks import CallbackManagerForRetrieverRun from langchain_core.documents import Document from langchain_core.embeddings.embeddings import Embeddings from langchain_core.retrievers import BaseRetriever -import os +from opensearchpy import OpenSearch + +# from elasticsearch.helpers import scan +from opensearchpy.helpers import scan from redbox.models.chain import RedboxState from redbox.models.file import ChunkResolution -from redbox.retriever.queries import add_document_filter_scores_to_query, build_document_query, get_all, get_metadata +from redbox.retriever.queries import ( + add_document_filter_scores_to_query, + build_document_query, + get_all, + get_metadata, + get_minimum_metadata, +) from redbox.transform import merge_documents, sort_documents logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO")) @@ -277,3 +282,29 @@ def _get_relevant_documents( ] return sorted(results, key=lambda result: result.metadata["index"]) + + +class BasicMetadataRetriever(OpenSearchRetriever): + """A modified MetadataRetriever that retrieves only filename, keyword and description metadata""" + + chunk_resolution: ChunkResolution = ChunkResolution.largest + + def __init__(self, es_client: Union[Elasticsearch, OpenSearch], **kwargs: Any) -> None: + # Hack to pass validation before overwrite + # Partly necessary due to how .with_config() interacts with a retriever + kwargs["body_func"] = get_minimum_metadata + kwargs["es_client"] = es_client + super().__init__(**kwargs) + self.body_func = partial(get_minimum_metadata, self.chunk_resolution) + + def _get_relevant_documents( + self, query: RedboxState, *, run_manager: CallbackManagerForRetrieverRun + ) -> list[Document]: # noqa:ARG002 + # if not self.es_client or not self.document_mapper: + # msg = "faulty configuration" + # raise ValueError(msg) # should not happen + + body = self.body_func(query) # type: ignore + response = self.es_client.search(index=self.index_name, body=body) + hits = response.get("hits", {}).get("hits", []) + return [hit["_source"] for hit in hits] From 9d4550150f034efae06e429f638fbc206314f10e Mon Sep 17 00:00:00 2001 From: Saisakul Chernbumroong <159020482+saisakul@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:05:39 +0000 Subject: [PATCH 2/5] turn self route using env (#121) * turn self route using env * add SELF_ROUTE_ENABLED to env.example --------- Co-authored-by: Saisakul Chernbumroong --- .env.example | 3 ++- redbox-core/redbox/models/chain.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 869c7ad04..b8a42eae6 100644 --- a/.env.example +++ b/.env.example @@ -102,4 +102,5 @@ REDBOX_API_KEY = myapi # AUTHBROKER_CLIENT_SECRET=REPLACE_WITH_GITLAB_SECRET # AUTHBROKER_URL=https://sso.trade.gov.uk -ENABLE_METADATA_EXTRACTION = True \ No newline at end of file +ENABLE_METADATA_EXTRACTION = True +SELF_ROUTE_ENABLED = False \ No newline at end of file diff --git a/redbox-core/redbox/models/chain.py b/redbox-core/redbox/models/chain.py index 16eaaff80..a886e829c 100644 --- a/redbox-core/redbox/models/chain.py +++ b/redbox-core/redbox/models/chain.py @@ -5,6 +5,8 @@ from typing import Annotated, Literal, NotRequired, Required, TypedDict, get_args, get_origin from uuid import UUID, uuid4 +import environ +from dotenv import load_dotenv from langchain_core.documents import Document from langchain_core.messages import AnyMessage from langgraph.graph.message import add_messages @@ -14,6 +16,9 @@ from redbox.models import prompts from redbox.models.settings import ChatLLMBackend +load_dotenv() +env = environ.Env() + class ChainChatMessage(TypedDict): role: Literal["user", "ai", "system"] @@ -29,7 +34,7 @@ class AISettings(BaseModel): # Prompts and LangGraph settings max_document_tokens: int = 1_000_000 - self_route_enabled: bool = False + self_route_enabled: bool = env.bool("SELF_ROUTE_ENABLED", default=False) map_max_concurrency: int = 128 stuff_chunk_context_ratio: float = 0.75 recursion_limit: int = 50 From 37095ced828a5c74726b4f4724508809ce80b6e2 Mon Sep 17 00:00:00 2001 From: Nora Er-Rouhly Date: Thu, 27 Feb 2025 11:20:23 +0000 Subject: [PATCH 3/5] Bugfix/search irrelevant query (#118) * remove bm25 query * understanding queries * move notebook * moving notebook to notebooks folder --------- Co-authored-by: nora-errouhly --- notebooks/composite_query_opensearch.ipynb | 479 ++++++++++++++++++ .../read_local_opensearch.ipynb | 0 redbox-core/redbox/retriever/queries.py | 34 +- 3 files changed, 480 insertions(+), 33 deletions(-) create mode 100644 notebooks/composite_query_opensearch.ipynb rename read_local_opensearch.ipynb => notebooks/read_local_opensearch.ipynb (100%) diff --git a/notebooks/composite_query_opensearch.ipynb b/notebooks/composite_query_opensearch.ipynb new file mode 100644 index 000000000..f23d8ef1e --- /dev/null +++ b/notebooks/composite_query_opensearch.ipynb @@ -0,0 +1,479 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Context" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When sending an irrelevant query such as \"what is the door about\", the search still returns irrelevant chunks from the selected document. The LLM intermittently hallucinate and answer the question based on the irrelevant chunks. This notebook is about how to optimize search but further analysis is required to ensure the LLM does not hallucinate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from opensearchpy import OpenSearch, RequestsHttpConnection, client\n", + "#\"http://admin:admin@opensearch:9200\"\n", + "client = OpenSearch(\n", + " hosts=[{\"host\": \"localhost\", \"port\": \"9200\"}],\n", + " http_auth=(\"admin\", \"admin\"),\n", + " use_ssl=False, #to run locally, changed from True to False\n", + " connection_class=RequestsHttpConnection,\n", + " retry_on_timeout=True\n", + " )\n", + "\n", + "query = {\n", + " \"size\": 1000,\n", + " \"track_total_hits\": True,\n", + " \"query\" : {\n", + " \"match_all\" : {}\n", + " }\n", + "}\n", + "\n", + "#redbox-data-integration-chunk-current\n", + "\n", + "response = client.search(index='redbox-data-integration-chunk-current', body=query)\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define QUERY" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.embeddings import BedrockEmbeddings\n", + "embedding_model = BedrockEmbeddings(region_name='eu-west-2', model_id=\"amazon.titan-embed-text-v2:0\")\n", + "#query = \"Data feminism begins by examining how power operates in the world today\" #66\n", + "#query = \"goodbye\" #score is 0\n", + "#query = \"what is this door about\" #score is 3.3\n", + "query = \"I don't know.\"\n", + "query_vector = embedding_model.embed_query(query)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client.indices.get_mapping(index='redbox-data-integration-chunk-current')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from pydantic import BaseModel\n", + "class AISettings(BaseModel):\n", + " \"\"\"Prompts and other AI settings\"\"\"\n", + "\n", + " # LLM settings\n", + " context_window_size: int = 128_000\n", + " llm_max_tokens: int = 1024\n", + "\n", + " # Prompts and LangGraph settings\n", + " max_document_tokens: int = 1_000_000\n", + " self_route_enabled: bool = False\n", + " map_max_concurrency: int = 128\n", + " stuff_chunk_context_ratio: float = 0.75\n", + " recursion_limit: int = 50\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " # Elasticsearch RAG and boost values\n", + " rag_k: int = 30\n", + " rag_num_candidates: int = 10\n", + " rag_gauss_scale_size: int = 3\n", + " rag_gauss_scale_decay: float = 0.5\n", + " rag_gauss_scale_min: float = 1.1\n", + " rag_gauss_scale_max: float = 2.0\n", + " elbow_filter_enabled: bool = False\n", + " match_boost: float = 1.0\n", + " match_name_boost: float = 2.0\n", + " match_description_boost: float = 0.5\n", + " match_keywords_boost: float = 0.5\n", + " knn_boost: float = 2.0\n", + " similarity_threshold: float = 0.7\n", + "\n", + " # this is also the azure_openai_model\n", + " #chat_backend: ChatLLMBackend = ChatLLMBackend()\n", + "\n", + " # settings for tool call\n", + " tool_govuk_retrieved_results: int = 100\n", + " tool_govuk_returned_results: int = 5" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "ai_settings = AISettings()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "query_filter = [{\n", + " \"bool\": {\n", + " \"should\": [\n", + " {\"terms\": {\"metadata.file_name.keyword\": ['natasha.boyse@digital.trade.gov.uk/1_The_power_chapter.pdf']}},\n", + " {\"terms\": {\"metadata.uri.keyword\": ['natasha.boyse@digital.trade.gov.uk/1_The_power_chapter.pdf']}}\n", + " ]\n", + " }\n", + " }, {\"term\": {\"metadata.chunk_resolution.keyword\": \"normal\"}}]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Composite query" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "final_query = {\"size\": ai_settings.rag_k,\n", + " \"query\": {\n", + " \"bool\": {\n", + " \"should\": [\n", + " {\n", + " \"match\": {\n", + " \"text\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_boost,\n", + " }\n", + " },\n", + " },\n", + " {\n", + " \"match\": {\n", + " \"metadata.name\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_name_boost,\n", + " }\n", + " }\n", + " },\n", + " {\n", + " \"match\": {\n", + " \"metadata.description\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_description_boost,\n", + " }\n", + " }\n", + " },\n", + " {\n", + " \"match\": {\n", + " \"metadata.keywords\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_keywords_boost,\n", + " }\n", + " }\n", + " },\n", + " {\n", + " \"knn\": {\n", + " \"vector_field\": {\n", + " \"vector\": query_vector,\n", + " \"k\": ai_settings.rag_num_candidates,\n", + " \"boost\": ai_settings.knn_boost}\n", + " }\n", + " },\n", + " ],\n", + " \"filter\": query_filter,\n", + " }\n", + " },\n", + " }\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "final_response = client.search(index='redbox-data-integration-chunk-current', body=final_query)\n", + "final_response" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Keyword query only" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "BM25 search on document text, title, description and keywords" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "keyword_final_query = {\"size\": ai_settings.rag_k,\n", + " \"query\": {\n", + " \"bool\": {\n", + " \"should\": [\n", + " {\n", + " \"match\": {\n", + " \"text\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_boost,\n", + " }\n", + " },\n", + " },\n", + " {\n", + " \"match\": {\n", + " \"metadata.name\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_name_boost,\n", + " }\n", + " }\n", + " },\n", + " {\n", + " \"match\": {\n", + " \"metadata.description\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_description_boost,\n", + " }\n", + " }\n", + " },\n", + " {\n", + " \"match\": {\n", + " \"metadata.keywords\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_keywords_boost,\n", + " }\n", + " }\n", + " },\n", + " ],\n", + " \"filter\": query_filter,\n", + " }\n", + " },\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response_keyword = client.search(index='redbox-data-integration-chunk-current', body=keyword_final_query)\n", + "response_keyword" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "BM25 query on document text only" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "text_keyword_final_query = {\"size\": ai_settings.rag_k,\n", + " #\"min_score\":0.01,\n", + " \"query\": {\n", + " \"bool\": {\n", + " \"should\": [\n", + " {\n", + " \"match\": {\n", + " \"text\": {\n", + " \"query\": query,\n", + " \"boost\": ai_settings.match_boost,\n", + " #\"analyzer\": \"stop\",\n", + " \n", + " }\n", + " },\n", + " }\n", + " ],\n", + " \"filter\": query_filter,\n", + " }\n", + " },\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response_text_keyword = client.search(index='redbox-data-integration-chunk-current', body=text_keyword_final_query)\n", + "response_text_keyword" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Semantic query" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "knn_final_query = {\"size\": ai_settings.rag_k,\n", + " #\"min_score\": 1.9,\n", + " \"query\": {\n", + " \"bool\": {\n", + " \"must\": [\n", + " {\n", + " \"knn\": {\n", + " \"vector_field\": {\n", + " \"vector\": query_vector,\n", + " \"k\": ai_settings.rag_num_candidates,\n", + " \"boost\": ai_settings.knn_boost,\n", + " \n", + " }\n", + " }\n", + " },\n", + " ],\n", + " \"filter\": query_filter,\n", + " }\n", + " },\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response_knn = client.search(index='redbox-data-integration-chunk-current', body=knn_final_query)\n", + "response_knn" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response_knn[\"hits\"][\"hits\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recommendations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- Keyword search based on BM25 does not remove stop words. This lead to inflated scores returning irrelevant results. Analyzer function in Opensearch should be used to remove stopwords. However, when applying analyzer only on query, it still returns irrelevant chunks. This could be due to the fact that we should also remove stop words from indexed documents. However, removing stop words would impact semantic search. Therefore, adding a new field for 'Text' attribute is required for keyword search where STOP analyzer is performed\n", + "\n", + "- Even when score is 0, Keyword search returns the chunks. We should set min_score for keyword search to a low value to filter out irrelevant chunks. When the query is irrelevant, semantic search does not return any chunks. Perhaps, there is in-built cutoff threeshold for the relevance score in Opensearch KNN but not in BM25. This need to be verified.\n", + "\n", + "- Relevance scores from BM25 are added to relevance scores from Semantic seach (cosine similarity). Scores from BM25 can be as high as 66 while score from Opensearch scaled cosine similarity is between 0 and 2.\n", + "Thereore, the impact of keyword is greater than semantic search. It doesn'\\t make sense to add both scores. In addition, there are scaling factors (Boost parameters) used as a multiplier to each score, to add more weight to to Semantic search and keyword search for the titles of the documents. The impact of such boosting scores need to be investigated. Further research on the best approach to implement hybrid search is required.\n", + "\n", + "- In the short term, we can remove keyword search and keep semantic search. This would address the issues with irrelevant queries, increasing the recall and therefore accuracy of the search. However, semantic search does not handle well acronyms. A long-term solution using hybrid search is required.\n", + "\n", + "- When implementing the short term solution, integration testing should be done to verify that the RAG and gadget/agent works when no chunks are returned:\n", + " 1) Ensuring code handles the edge case where no chunks returned.\n", + " 2) Ensuring LLM do not hallucinate if there are no chunks returned\n", + " 2) Ensuring agent/gadget select other tools (for example gov.uk) to attempt answering the question" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"The bool query takes a more-matches-is-better approach, so the score from each matching must or should clause will be added together to provide the final _score for each document.\"\n", + "https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html#:~:text=The%20bool%20query%20takes%20a,final%20_score%20for%20each%20document.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Stop analyzers\n", + "https://opensearch.org/docs/2.0/opensearch/query-dsl/text-analyzers/" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"The search term is analyzed by the same analyzer that was used for the specific document field at the time it was indexed. This means that your search term goes through the same analysis process as the document’s field.\"\n", + "https://opensearch.org/docs/latest/query-dsl/term-vs-full-text/\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/read_local_opensearch.ipynb b/notebooks/read_local_opensearch.ipynb similarity index 100% rename from read_local_opensearch.ipynb rename to notebooks/read_local_opensearch.ipynb diff --git a/redbox-core/redbox/retriever/queries.py b/redbox-core/redbox/retriever/queries.py index 68600965e..e5d67fab4 100644 --- a/redbox-core/redbox/retriever/queries.py +++ b/redbox-core/redbox/retriever/queries.py @@ -132,39 +132,7 @@ def build_document_query( "size": ai_settings.rag_k, "query": { "bool": { - "should": [ - { - "match": { - "text": { - "query": query, - "boost": ai_settings.match_boost, - } - }, - }, - { - "match": { - "metadata.name": { - "query": query, - "boost": ai_settings.match_name_boost, - } - } - }, - { - "match": { - "metadata.description": { - "query": query, - "boost": ai_settings.match_description_boost, - } - } - }, - { - "match": { - "metadata.keywords": { - "query": query, - "boost": ai_settings.match_keywords_boost, - } - } - }, + "must": [ { "knn": { "vector_field": { From 39ded622943529a51ba99cbfa9dbb547c26b66b8 Mon Sep 17 00:00:00 2001 From: Saisakul Chernbumroong <159020482+saisakul@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:32:30 +0000 Subject: [PATCH 4/5] Add summarise graph (#115) * add summarise graph * add new summarise graph * add keyword to test --------- Co-authored-by: Saisakul Chernbumroong --- redbox-core/redbox/graph/root.py | 130 +++++++++++++++++++++++++++- redbox-core/redbox/models/chat.py | 1 + redbox-core/redbox/models/graph.py | 1 + redbox-core/tests/graph/test_app.py | 2 +- 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/redbox-core/redbox/graph/root.py b/redbox-core/redbox/graph/root.py index 5aee48e67..d632ffb50 100644 --- a/redbox-core/redbox/graph/root.py +++ b/redbox-core/redbox/graph/root.py @@ -5,6 +5,7 @@ from langgraph.graph import END, START, StateGraph from langgraph.graph.graph import CompiledGraph from langgraph.prebuilt import ToolNode +from langgraph.pregel import RetryPolicy from redbox.chains.components import get_structured_response_with_citations_parser from redbox.chains.runnables import build_self_route_output_parser @@ -37,7 +38,132 @@ from redbox.models.chat import ChatRoute, ErrorRoute from redbox.models.graph import ROUTABLE_KEYWORDS, RedboxActivityEvent from redbox.transform import structure_documents_by_file_name, structure_documents_by_group_and_indices -from langgraph.pregel import RetryPolicy + + +def get_summarise_graph(all_chunks_retriever): + builder = StateGraph(RedboxState) + builder.add_node("choose_route_based_on_request_token", empty_process) + builder.add_node("set_route_to_summarise_large_doc", build_set_route_pattern(ChatRoute.chat_with_docs_map_reduce)) + builder.add_node("set_route_to_summarise_doc", build_set_route_pattern(ChatRoute.chat_with_docs)) + builder.add_node("pass_user_prompt_to_LLM_message", build_passthrough_pattern()) + builder.add_node("clear_documents", clear_documents_process) + + builder.add_node("document_has_multiple_chunks", empty_process) + builder.add_node("any_summarised_docs_bigger_than_context_window", empty_process) + builder.add_node("any_document_bigger_than_context_window", empty_process) + + builder.add_node("sending_chunks_to_summarise", empty_process) + builder.add_node("sending_summarised_chunks_checking_exceed_context", empty_process) + builder.add_node("sending_summarised_chunks", empty_process) + + builder.add_node( + "summarise_summarised_chunks", + build_merge_pattern(prompt_set=PromptSet.ChatwithDocsMapReduce), + retry=RetryPolicy(max_attempts=3), + ) + + builder.add_node( + "files_too_large_error", + build_error_pattern( + text="These documents are too large to work with.", + route_name=ErrorRoute.files_too_large, + ), + ) + + builder.add_node( + "retrieve_all_chunks", + build_retrieve_pattern( + retriever=all_chunks_retriever, + structure_func=structure_documents_by_file_name, + final_source_chain=True, + ), + ) + builder.add_node( + "summarise_each_chunk_in_document", + build_merge_pattern(prompt_set=PromptSet.ChatwithDocsMapReduce), + retry=RetryPolicy(max_attempts=3), + ) + + # edges + builder.add_edge(START, "choose_route_based_on_request_token") + builder.add_conditional_edges( + "choose_route_based_on_request_token", + build_total_tokens_request_handler_conditional(PromptSet.ChatwithDocsMapReduce), + { + "max_exceeded": "files_too_large_error", + "context_exceeded": "set_route_to_summarise_large_doc", + "pass": "set_route_to_summarise_doc", + }, + ) + builder.add_edge("set_route_to_summarise_large_doc", "pass_user_prompt_to_LLM_message") + builder.add_edge("set_route_to_summarise_doc", "pass_user_prompt_to_LLM_message") + builder.add_edge("pass_user_prompt_to_LLM_message", "retrieve_all_chunks") + builder.add_conditional_edges( + "retrieve_all_chunks", + lambda s: s.route_name, + { + ChatRoute.chat_with_docs: "summarise_document", + ChatRoute.chat_with_docs_map_reduce: "sending_chunks_to_summarise", + }, + ) + + # summarise process + builder.add_node( + "summarise_document", + build_stuff_pattern( + prompt_set=PromptSet.ChatwithDocs, + final_response_chain=True, + ), + retry=RetryPolicy(max_attempts=3), + ) + builder.add_edge("summarise_document", "clear_documents") + + # summarise large documents process + builder.add_conditional_edges( + "sending_chunks_to_summarise", + build_document_chunk_send("summarise_each_chunk_in_document"), + path_map=["summarise_each_chunk_in_document"], + ) + builder.add_edge("summarise_each_chunk_in_document", "document_has_multiple_chunks") + builder.add_conditional_edges( + "document_has_multiple_chunks", + multiple_docs_in_group_conditional, + { + True: "sending_summarised_chunks_checking_exceed_context", + False: "any_summarised_docs_bigger_than_context_window", + }, + ) + builder.add_conditional_edges( + "sending_summarised_chunks_checking_exceed_context", + build_document_group_send("any_document_bigger_than_context_window"), + path_map=["any_document_bigger_than_context_window"], + ) + builder.add_conditional_edges( + "any_document_bigger_than_context_window", + build_documents_bigger_than_context_conditional(PromptSet.ChatwithDocsMapReduce), + { + True: "files_too_large_error", + False: "sending_summarised_chunks", + }, + ) + builder.add_conditional_edges( + "sending_summarised_chunks", + build_document_group_send("summarise_summarised_chunks"), + path_map=["summarise_summarised_chunks"], + ) + builder.add_edge("summarise_summarised_chunks", "any_summarised_docs_bigger_than_context_window") + builder.add_conditional_edges( + "any_summarised_docs_bigger_than_context_window", + build_documents_bigger_than_context_conditional(PromptSet.ChatwithDocs), + { + True: "files_too_large_error", + False: "summarise_document", + }, + ) + builder.add_edge("summarise_document", "clear_documents") + builder.add_edge("clear_documents", END) + builder.add_edge("files_too_large_error", END) + return builder.compile() def get_self_route_graph(retriever: VectorStoreRetriever, prompt_set: PromptSet, debug: bool = False): @@ -434,6 +560,7 @@ def get_root_graph( builder.add_node("p_chat_with_documents", cwd_subgraph) builder.add_node("p_retrieve_metadata", metadata_subgraph) builder.add_node("p_new_route", new_route) + builder.add_node("p_summarise", get_summarise_graph(all_chunks_retriever)) # Log builder.add_node( @@ -464,6 +591,7 @@ def get_root_graph( ChatRoute.search: "p_search", ChatRoute.gadget: "p_search_agentic", ChatRoute.newroute: "p_new_route", + ChatRoute.summarise: "p_summarise", "DEFAULT": "d_docs_selected", }, ) diff --git a/redbox-core/redbox/models/chat.py b/redbox-core/redbox/models/chat.py index 71b79a8e9..c560863ab 100644 --- a/redbox-core/redbox/models/chat.py +++ b/redbox-core/redbox/models/chat.py @@ -8,6 +8,7 @@ class ChatRoute(StrEnum): chat_with_docs = "summarise" chat_with_docs_map_reduce = "chat/documents/large" newroute = "newroute" + summarise = "summarise" class ErrorRoute(StrEnum): diff --git a/redbox-core/redbox/models/graph.py b/redbox-core/redbox/models/graph.py index ae37a74f4..fdf29dbd1 100644 --- a/redbox-core/redbox/models/graph.py +++ b/redbox-core/redbox/models/graph.py @@ -24,4 +24,5 @@ class RedboxActivityEvent(BaseModel): ChatRoute.search: "Search for an answer to the question in the document", ChatRoute.gadget: "Let Redbox go-go-gadget to answer to the question using the documents", ChatRoute.newroute: "New amazing route", + ChatRoute.summarise: "Summarise documents", } diff --git a/redbox-core/tests/graph/test_app.py b/redbox-core/tests/graph/test_app.py index 84ba43c82..d25794b71 100644 --- a/redbox-core/tests/graph/test_app.py +++ b/redbox-core/tests/graph/test_app.py @@ -604,6 +604,6 @@ def test_get_available_keywords(env: Settings): env=env, debug=LANGGRAPH_DEBUG, ) - keywords = {ChatRoute.search, ChatRoute.newroute, ChatRoute.gadget} + keywords = {ChatRoute.search, ChatRoute.newroute, ChatRoute.gadget, ChatRoute.summarise} assert keywords == set(app.get_available_keywords().keys()) From e598a5fd3899505334c2aca9dcbd2fa4642b03fb Mon Sep 17 00:00:00 2001 From: Tash Boyse <57753415+nboyse@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:38:55 +0000 Subject: [PATCH 5/5] feat: Copy Paste into chat box retains all formatting (#119) --- django_app/frontend/src/chat-styles.scss | 8 ++++++++ .../src/js/web-components/chats/message-input.js | 10 +++++----- django_app/redbox_app/templates/chats.html | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/django_app/frontend/src/chat-styles.scss b/django_app/frontend/src/chat-styles.scss index 77a5de6ca..917a50072 100644 --- a/django_app/frontend/src/chat-styles.scss +++ b/django_app/frontend/src/chat-styles.scss @@ -591,6 +591,14 @@ main:has(.iai-chat-bubble) .chat-options { } } +.iai-chat-input__input { + background-color: white; + border: #767676 1px solid; + min-height: 1.5em; + max-height: 200px; + overflow-y: auto; +} + .exit-feedback { background-color: #f3edc9; display: none; diff --git a/django_app/frontend/src/js/web-components/chats/message-input.js b/django_app/frontend/src/js/web-components/chats/message-input.js index d80109de8..ccbccda26 100644 --- a/django_app/frontend/src/js/web-components/chats/message-input.js +++ b/django_app/frontend/src/js/web-components/chats/message-input.js @@ -3,7 +3,7 @@ export class MessageInput extends HTMLElement { constructor() { super(); - this.textarea = this.querySelector("textarea"); + this.textarea = this.querySelector(".iai-chat-input__input"); } connectedCallback() { @@ -15,7 +15,7 @@ export class MessageInput extends HTMLElement { this.textarea.addEventListener("keypress", (evt) => { if (evt.key === "Enter" && !evt.shiftKey && this.textarea) { evt.preventDefault(); - if (this.textarea.value.trim()) { + if (this.textarea?.textContent?.trim()) { this.closest("form")?.requestSubmit(); } } @@ -32,7 +32,7 @@ export class MessageInput extends HTMLElement { return; } this.textarea.style.height = "auto"; - this.textarea.style.height = `${this.textarea.scrollHeight}px`; + this.textarea.style.height = `${this.textarea.scrollHeight || this.textarea.offsetHeight}px`; }; /** @@ -40,7 +40,7 @@ export class MessageInput extends HTMLElement { * @returns string */ getValue = () => { - return this.querySelector("textarea")?.value.trim() || ""; + return this.textarea?.textContent?.trim() || ""; }; /** @@ -50,7 +50,7 @@ export class MessageInput extends HTMLElement { if (!this.textarea) { return; } - this.textarea.value = ""; + this.textarea.textContent = ""; this.#adjustHeight(); }; } diff --git a/django_app/redbox_app/templates/chats.html b/django_app/redbox_app/templates/chats.html index 3e503ada7..a22996eef 100644 --- a/django_app/redbox_app/templates/chats.html +++ b/django_app/redbox_app/templates/chats.html @@ -154,7 +154,7 @@ Message Redbox - +