1 line
20 KiB
JSON
1 line
20 KiB
JSON
{"repo": "albertouah/castena", "files": {"src/prod/utils.py": {"de59e384038c2b226fdf0a0b41e074da77ca8cb2": {"t": 1785674497.3735328, "n": 19892, "h": "a4d0aa504db23c84", "c": "# -- Utils .py file\n# -- Libraries\nfrom typing import Any, Dict, List, Mapping, Optional\nfrom pydantic import Extra, Field, root_validator\nfrom langchain_community.vectorstores import FAISS\nfrom langchain_core.runnables import RunnablePassthrough\nfrom langchain.llms.base import LLM\nfrom langchain.chat_models import ChatOpenAI\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain.prompts import PromptTemplate\nfrom langchain.schema import StrOutputParser\nfrom langchain.utils import get_from_dict_or_env\nfrom langchain.vectorstores import Chroma\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter\nfrom langchain.chains import RetrievalQA, MapReduceDocumentsChain, ReduceDocumentsChain\nfrom langchain.document_loaders import TextLoader\nfrom langchain.embeddings import HuggingFaceEmbeddings, OpenAIEmbeddings\nfrom langchain.chains import LLMChain\nfrom langchain.evaluation import StringEvaluator\nfrom typing import Any, Optional\nfrom langsmith.client import Client\nfrom langchain.smith import RunEvalConfig, run_on_dataset\nfrom langchain.chains.combine_documents.stuff import StuffDocumentsChain\nimport streamlit as st\nimport together\nimport textwrap\nimport getpass\nimport spacy\nimport os\nimport re\n\n#os.environ[\"TOGETHER_API_KEY\"] = \"6101599d6e33e3bda336b8d007ca22e35a64c72cfd52c2d8197f663389fc50c5\"\n#os.environ[\"OPENAI_API_KEY\"] = \"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\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n\nclient = Client()\n\n# -- LLM class\nclass TogetherLLM(LLM):\n \"\"\"Together large language models.\"\"\"\n\n model: str = \"togethercomputer/llama-2-70b-chat\"\n \"\"\"model endpoint to use\"\"\"\n\n together_api_key: str = os.environ[\"TOGETHER_API_KEY\"]\n \"\"\"Together API key\"\"\"\n\n temperature: float = 0.7\n \"\"\"What sampling temperature to use.\"\"\"\n\n max_tokens: int = 512\n \"\"\"The maximum number of tokens to generate in the completion.\"\"\"\n\n original_transcription: str = \"\"\n \"\"\"Original transcription\"\"\"\n\n class Config:\n extra = Extra.forbid\n\n #@root_validator(skip_on_failure=True)\n def validate_environment(cls, values: Dict) -> Dict:\n \"\"\"Validate that the API key is set.\"\"\"\n api_key = get_from_dict_or_env(\n values, \"together_api_key\", \"TOGETHER_API_KEY\"\n )\n values[\"together_api_key\"] = api_key\n return values\n\n @property\n def _llm_type(self) -> str:\n \"\"\"Return type of LLM.\"\"\"\n return \"together\"\n\n def clean_duplicates(self, transcription: str) -> str:\n transcription = transcription.strip().replace('/n/n ', \"\"\"\n\"\"\")\n new_transcription_aux = []\n for text in transcription.split('\\n\\n'):\n if text not in new_transcription_aux:\n is_substring = any(transcription_aux.replace('\"', '').lower() in text.replace('\"', '').lower()\\\n for transcription_aux in new_transcription_aux)\n if not is_substring:\n new_transcription_aux.append(text)\n return '\\n\\n'.join(new_transcription_aux)\n\n def _call(\n self,\n prompt: str,\n **kwargs: Any,\n ) -> str:\n \"\"\"Call to Together endpoint.\"\"\"\n regex_transcription = r'CONTEXTO:(\\n.*)+PREGUNTA'\n regex_init_transcription = r\"Desde el instante [0-9]+:[0-9]+:[0-9]+(?:\\.[0-9]+)? hasta [0-9]+:[0-9]+:[0-9]+(?:\\.[0-9]+)? [a-zA-Záéíóú ]+ dice: ?\"\n\n # -- Extract transcription\n together.api_key = self.together_api_key\n cleaned_prompt = self.clean_duplicates(prompt)\n resultado = re.search(regex_transcription, cleaned_prompt, re.DOTALL)\n\n resultado = re.sub(regex_init_transcription, \"\", resultado.group(1).strip()).replace('\\\"', '')\n resultado_alpha_num = [re.sub(r'\\W+', ' ', resultado_aux).strip().lower() for resultado_aux in resultado.split('\\n\\n')]\n\n # -- Setup new transcription format, without duplicates and with its correspondent speaker\n new_transcription = []\n for transcription in self.original_transcription.split('\\n\\n'):\n transcription_cleaned = re.sub(regex_init_transcription, \"\", transcription.strip()).replace('\\\"', '')\n transcription_cleaned = re.sub(r'\\W+', ' ', transcription_cleaned).strip().lower()\n for resultado_aux in resultado_alpha_num:\n if resultado_aux in transcription_cleaned:\n init_transcription = re.search(regex_init_transcription, transcription).group(0)\n new_transcription.append(init_transcription + '\\\"' + resultado_aux + '\\\"')\n # -- Merge with original transcription\n new_transcription = '\\n\\n'.join(list(set(new_transcription)))\n new_cleaned_prompt = re.sub(regex_transcription, f\"\"\"CONTEXTO:\n{new_transcription}\nPREGUNTA:\"\"\", cleaned_prompt, re.DOTALL)\n print(new_cleaned_prompt)\n output = together.Complete.create(new_cleaned_prompt,\n model=self.model,\n max_tokens=self.max_tokens,\n temperature=self.temperature,\n )\n text = output['output']['choices'][0]['text']\n text = self.clean_duplicates(text)\n return text\n\n# -- Langchain evaluator\nclass RelevanceEvaluator(StringEvaluator):\n \"\"\"An LLM-based relevance evaluator.\"\"\"\n\n def __init__(self):\n llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n\n template = \"\"\"En una escala del 0 al 100, ¿Como de relevante es la siguiente salida con respecto a la siguiente entrada?\n --------\n ENTRADA: {input}\n --------\n SALIDA: {prediction}\n --------\n Razona paso a paso porqué el score que has elegido es apropiado y despues muestra la puntuacion al final.\"\"\"\n\n self.eval_chain = LLMChain.from_string(llm=llm, template=template)\n\n @property\n def requires_input(self) -> bool:\n return True\n\n @property\n def requires_reference(self) -> bool:\n return False\n\n @property\n def evaluation_name(self) -> str:\n return \"scored_relevance\"\n\n def _evaluate_strings(\n self,\n prediction: str,\n input: Optional[str] = None,\n reference: Optional[str] = None,\n **kwargs: Any\n ) -> dict:\n evaluator_result = self.eval_chain(\n dict(input=input, prediction=prediction), **kwargs\n )\n reasoning, score = evaluator_result[\"text\"].split(\"\\n\", maxsplit=1)\n score = re.search(r\"\\d+\", score).group(0)\n if score is not None:\n score = float(score.strip()) / 100.0\n return {\"score\": score, \"reasoning\": reasoning.strip()}\n\n# -- Get GPT response\ndef get_gpt_response(transcription_path, query, logger):\n template = \"\"\"Eres un asistente. Su misión es proporcionar respuestas precisas a preguntas relacionadas con la transcripción de una entrevista de YouTube.\n No saludes en tu respuesta. No repita la pregunta en su respuesta. Sea conciso y omita las exenciones de responsabilidad o los mensajes predeterminados.\n Solo responda la pregunta, no agregue texto adicional. No des tu opinión personal ni tu conclusión personal. No haga conjeturas ni suposiciones.\n Si no sabe la respuesta de la pregunta o el contexto está vacío, responda cortésmente por qué no sabe la respuesta. Por favor no comparta información falsa.\n {context}\n Pregunta: {question}\n Respuesta:\"\"\"\n \n rag_prompt_custom = PromptTemplate.from_template(template)\n loader = TextLoader(transcription_path)\n docs = loader.load()\n\n text_splitter = RecursiveCharacterTextSplitter(chunk_size=3000, chunk_overlap=250, length_function=len)\n splits = text_splitter.split_documents(docs)\n \n vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())\n retriever = vectorstore.as_retriever()\n \n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0) \n \n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n \n rag_chain = (\n {\"context\": retriever, \"question\": RunnablePassthrough()}\n | rag_prompt_custom\n | llm\n | StrOutputParser()\n )\n llm_output = rag_chain.invoke(query)\n return llm_output\n\ndef get_character_info_gpt(text, character):\n vectorstore = FAISS.from_texts(\n [text], embedding=OpenAIEmbeddings()\n )\n retriever = vectorstore.as_retriever()\n \n template = \"\"\"Responde a la siguiente pregunta basandote unicamente en el siguiente contexto:\n {context}\n \n Pregunta: {question}\n \"\"\"\n prompt = ChatPromptTemplate.from_template(template)\n \n model = ChatOpenAI()\n\n chain = (\n {\"context\": retriever, \"question\": RunnablePassthrough()}\n | prompt\n | model\n | StrOutputParser()\n )\n return chain.invoke(\"¿Quien es {}?\".format(character))\n\n \n# -- Text summarisation with OpenAI (map-reduce technique)\ndef summarise_doc(transcription_path, model_name, model=None):\n if model_name == 'gpt':\n llm = ChatOpenAI(temperature=0, max_tokens=1024)\n \n # -- Map\n loader = TextLoader(transcription_path)\n docs = loader.load()\n map_template = \"\"\"Lo siguiente es listado de fragmentos de una conversacion:\n {docs}\n En base a este listado, por favor identifica los temas/topics principales.\n Respuesta:\"\"\"\n map_prompt = PromptTemplate.from_template(map_template)\n map_chain = LLMChain(llm=llm, prompt=map_prompt)\n \n # -- Reduce\n reduce_template = \"\"\"A continuacion se muestra un conjunto de resumenes:\n {docs}\n Usalos para crear un unico resumen consolidado de todos los temas/topics principales. \n Respuesta:\"\"\"\n reduce_prompt = PromptTemplate.from_template(reduce_template)\n \n # Run chain\n reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)\n \n # Takes a list of documents, combines them into a single string, and passes this to an LLMChain\n combine_documents_chain = StuffDocumentsChain(\n llm_chain=reduce_chain, document_variable_name=\"docs\"\n )\n \n # Combines and iteravely reduces the mapped documents\n reduce_documents_chain = ReduceDocumentsChain(\n # This is final chain that is called.\n combine_documents_chain=combine_documents_chain,\n # If documents exceed context for `StuffDocumentsChain`\n collapse_documents_chain=combine_documents_chain,\n # The maximum number of tokens to group documents into.\n token_max=3000,\n )\n \n # Combining documents by mapping a chain over them, then combining results\n map_reduce_chain = MapReduceDocumentsChain(\n # Map chain\n llm_chain=map_chain,\n # Reduce chain\n reduce_documents_chain=reduce_documents_chain,\n # The variable name in the llm_chain to put the documents in\n document_variable_name=\"docs\",\n # Return the results of the map steps in the output\n return_intermediate_steps=False,\n )\n \n text_splitter = CharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=3000, chunk_overlap=0\n )\n split_docs = text_splitter.split_documents(docs)\n doc_summary = map_reduce_chain.run(split_docs)\n else:\n # -- Keep original transcription\n with open(transcription_path, 'r') as f:\n docs = f.read()\n\n print(\"READ DOCUMENT\")\n print(docs)\n \n llm = TogetherLLM(\n model= model,\n temperature = 0.0,\n max_tokens = 1024,\n original_transcription = docs\n )\n \n # Map\n map_template = \"\"\"Lo siguiente es un extracto de una conversación entre dos hablantes en español.\n{docs}\nPor favor resuma la conversación en español.\nResumen:\"\"\"\n map_prompt = PromptTemplate(template=map_template, input_variables=[\"docs\"])\n map_chain = LLMChain(llm=llm, prompt=map_prompt)\n \n # Reduce\n reduce_template = \"\"\"Lo siguiente es una lista de resumenes en español:\n{doc_summaries}\nTómelos y descríbalos en un resumen final consolidado en español. Además, enumera los temas principales de la conversación en español.\n\nResumen:\"\"\"\n reduce_prompt = PromptTemplate(template=reduce_template, input_variables=[\"doc_summaries\"])\n \n # Run chain\n reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)\n \n # Takes a list of documents, combines them into a single string, and passes this to an LLMChain\n combine_documents_chain = StuffDocumentsChain(\n llm_chain=reduce_chain, document_variable_name=\"doc_summaries\"\n )\n \n # Combines and iteravely reduces the mapped documents\n reduce_documents_chain = ReduceDocumentsChain(\n # This is final chain that is called.\n combine_documents_chain=combine_documents_chain,\n # If documents exceed context for `StuffDocumentsChain`\n collapse_documents_chain=combine_documents_chain,\n # The maximum number of tokens to group documents into.\n verbose=True,\n token_max=1024\n )\n \n # Combining documents by mapping a chain over them, then combining results\n map_reduce_chain = MapReduceDocumentsChain(\n # Map chain\n llm_chain=map_chain,\n # Reduce chain\n reduce_documents_chain=reduce_documents_chain,\n # The variable name in the llm_chain to put the documents in\n document_variable_name=\"docs\",\n # Return the results of the map steps in the output\n return_intermediate_steps=False,\n verbose=True\n )\n text_splitter = CharacterTextSplitter(\n separator = \"\\n\\n\",\n chunk_size = 2000,\n chunk_overlap = 50,\n length_function = len,\n is_separator_regex = True,\n )\n split_docs = text_splitter.create_documents([docs])\n doc_summary = map_reduce_chain.run(split_docs)\n \n return doc_summary\n\n# -- Python function to setup basic features: SpaCy pipeline and LLM model\n@st.cache_resource\ndef setup_app(transcription_path, emb_model, model, _logger):\n # -- Setup enviroment and features\n nlp = spacy.load('es_core_news_lg')\n\n _logger.info('Setup environment and features...')\n\n # -- Setup LLM\n together.api_key = os.environ[\"TOGETHER_API_KEY\"]\n # List available models and descriptons\n models = together.Models.list()\n # Set llama2 7b LLM\n #together.Models.start(model)\n _logger.info('Setup environment and features - FINISHED!')\n\n # -- Read translated transcription\n _logger.info('Loading transcription...')\n loader = TextLoader(transcription_path)\n documents = loader.load()\n # Splitting the text into chunks\n text_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=100)\n texts = text_splitter.split_documents(documents)\n _logger.info('Loading transcription - FINISHED!')\n\n # -- Load embedding\n _logger.info('Loading embedding...')\n encode_kwargs = {'normalize_embeddings': True} # set True to compute cosine similarity\n model_norm = HuggingFaceEmbeddings(\n model_name=emb_model,\n model_kwargs={'device': 'cpu'},\n encode_kwargs=encode_kwargs\n )\n _logger.info('Loading embedding - FINISHED!')\n\n # -- Create document database\n _logger.info('Creating document database...')\n # Embed and store the texts\n # Supplying a persist_directory will store the embeddings on disk\n persist_directory = 'db'\n ## Here is the nmew embeddings being used\n embedding = model_norm\n\n vectordb = Chroma.from_documents(documents=texts,\n embedding=embedding,\n persist_directory=persist_directory)\n\n # -- Make a retreiver\n retriever = vectordb.as_retriever(search_kwargs={\"k\": 5})\n _logger.info('Creating document database - FINISHED!')\n _logger.info('Setup finished!')\n return nlp, retriever\n\n# -- Function to get prompt template\ndef get_prompt(instruction, system_prompt, b_sys, e_sys, b_inst, e_inst, _logger):\n new_system_prompt = b_sys + system_prompt + e_sys\n prompt_template = b_inst + new_system_prompt + instruction + e_inst\n _logger.info('Prompt template created: {}'.format(instruction))\n return prompt_template\n\n# -- Function to create the chain to answer questions\n@st.cache_resource\ndef create_llm_chain(model, _retriever, _chain_type_kwargs, _logger, transcription_path):\n _logger.info('Creating LLM chain...')\n # -- Keep original transcription\n with open(transcription_path, 'r') as f:\n formatted_transcription = f.read()\n \n llm = TogetherLLM(\n model= model,\n temperature = 0.0,\n max_tokens = 1024,\n original_transcription = formatted_transcription\n )\n qa_chain = RetrievalQA.from_chain_type(llm=llm,\n chain_type=\"stuff\",\n retriever=_retriever,\n chain_type_kwargs=_chain_type_kwargs,\n return_source_documents=True)\n _logger.info('Creating LLM chain - FINISHED!')\n return qa_chain\n\n# -------------------------------------------\n# -- Auxiliar functions\ndef wrap_text_preserve_newlines(text, width=110):\n # Split the input text into lines based on newline characters\n lines = text.split('\\n')\n\n # Wrap each line individually\n wrapped_lines = [textwrap.fill(line, width=width) for line in lines]\n\n # Join the wrapped lines back together using newline characters\n wrapped_text = '\\n'.join(wrapped_lines)\n\n return wrapped_text\n\ndef process_llm_response(llm_response):\n return wrap_text_preserve_newlines(llm_response)\n\n\ndef time_to_seconds(time_str):\n parts = time_str.split(':')\n hours, minutes, seconds = map(float, parts)\n return int((hours * 3600) + (minutes * 60) + seconds)\n\n# -- Extract seconds from transcription\ndef add_hyperlink_and_convert_to_seconds(text):\n time_pattern = r'(\\d{2}:\\d{2}:\\d{2}(?:.\\d{6})?)'\n \n def get_seconds(match, text):\n if len(match) == 2:\n start_time_str, end_time_str = match[0], match[1]\n else:\n start_time_str = match[0]\n end_time_str = re.findall(r\"Desde el instante {} hasta {}\".format(start_time_str, time_pattern), text)[0].split('hasta ')[-1]\n \n start_time_seconds = time_to_seconds(start_time_str)\n end_time_seconds = time_to_seconds(end_time_str)\n return start_time_str, start_time_seconds, end_time_str, end_time_seconds\n start_time_str, start_time_seconds, end_time_str, end_time_seconds = get_seconds(re.findall(time_pattern, text), text)\n return start_time_str, start_time_seconds, end_time_str, end_time_seconds\n\n# -- Streamlit HTML template\ndef typewrite(youtube_video_url, i=0):\n youtube_video_url = youtube_video_url.replace(\"?enablejsapi=1\", \"\")\n margin = \"{margin: 0;}\"\n html = f\"\"\"\n <html>\n <style>\n p {margin}\n </style>\n <body>\n <script src=\"https://www.youtube.com/player_api\"></script>\n <p align=\"center\">\n <iframe id=\"player_{i}\" src=\"{youtube_video_url}\" width=\"600\" height=\"450\"></iframe>\n </p>\n </body>\n </html>\n \"\"\"\n return html\n"}}}} |