imh0/Doodle
1
1import streamlit as st2import logging3import sys4import os5import re6from langchain.chat_models import ChatOpenAI7from langchain.llms import OpenAI8from crawlbase import CrawlingAPI9from langchain.output_parsers import StructuredOutputParser10from langchain.text_splitter import RecursiveCharacterTextSplitter11from langchain.embeddings.openai import OpenAIEmbeddings12from langchain.vectorstores import Qdrant13from langchain.prompts import ChatPromptTemplate14from elevenlabs import generate, play, set_api_key15from langchain.schema import (16 AIMessage,17 HumanMessage,18 SystemMessage19)20import random21from urllib.parse import urlparse, urlunparse22 23set_api_key(st.secrets["ELEVENLABS_API_KEY"])24crawling_api_key = st.secrets["CRAWLING_API_KEY"]25open_api_key = st.secrets["OPENAI_API_KEY"]26 27logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)28 29PAGE_TITLE: str = "Doodle"30PAGE_ICON: str = "๐จ๏ธ"31 32st.set_page_config(page_title=PAGE_TITLE, page_icon=PAGE_ICON)33 34 35def get_llm(model_name, model_temperature, api_key, max_tokens=None):36 if model_name == "text-davinci-003":37 return OpenAI(temperature=model_temperature, model_name=model_name, max_tokens=max_tokens,38 openai_api_key=api_key)39 else:40 return ChatOpenAI(temperature=model_temperature, model_name=model_name, max_tokens=max_tokens,41 openai_api_key=api_key)42 43 44def is_valid_web_link(url):45 parsed_url = urlparse(url)46 cleaned_url = parsed_url._replace(query='')._replace(params='')47 if parsed_url.scheme and parsed_url.netloc:48 return urlunparse(cleaned_url)49 else:50 return None51 52 53@st.cache_data54def scrape_the_article(url):55 api = CrawlingAPI({'token': crawling_api_key})56 response = api.get(url, options={'format': 'json', 'autoparse': 'true', 'scroll': 'true'})57 # dict_keys(['alert', 'title', 'favicon', 'meta', 'content', 'canonical', 'images', 'grouped_images', 'og_images', 'links'])58 content = response['json']59 return content60 61 62def init_session() -> None:63 if 'init' not in st.session_state:64 st.session_state.init = True65 st.session_state.question = None66 67 st.session_state.messages = []68 69 70@st.cache_data71def get_content_summary(content, model_name, api_key):72 llm = get_llm(model_name=model_name, model_temperature=0, api_key=api_key)73 format_instructions = \74 """75 The output should be a markdown code snippet formatted in the following schema, including the leading and trailing \\"```json\\" and \\"```\\":76 ```json{77 "summary": string // overall text summary 78 "blocks": [79 { 80 "block_summary": string // The summary of the first block81 "block_question": string // What is the question to clarify?82 }, ... 83 ]}84 ``` 85 """86 prompt_template = """You are an advanced copywriter who can discuss and summarise articles. Translate the text to English if required. You instructions: 1) Write a concise summary of the whole text; 2) Break down the text into logical blocks containing unique information, extract important information for each block and write a summary using this information; 3) Generate relevant critical questions related to each block; 4) Format the output according to format instructions. Here is the text:87 ``` {text} ```88 Format instructions: ``` {format_instructions} ``` 89 Answer:"""90 prompt = ChatPromptTemplate.from_template(template=prompt_template)91 messages = prompt.format_messages(text=content, format_instructions=format_instructions)92 logging.info(messages)93 response = llm(messages)94 logging.info(response)95 96 output_parser = StructuredOutputParser.from_response_schemas([])97 output_dict = output_parser.parse(response.content)98 return output_dict99 100 101@st.cache_data102def generate_audio(text):103 audio = generate(104 text=text,105 voice="Matthew" if random.randint(1, 10) % 2 == 0 else 'Dorothy',106 model="eleven_monolingual_v1"107 )108 return audio109 110 111@st.cache_resource112def get_retriever(content):113 text_splitter = RecursiveCharacterTextSplitter(114 chunk_size=300, # it depends on the retriever parameters and the model's context length115 chunk_overlap=20,116 length_function=len,117 is_separator_regex=False,118 )119 docs = text_splitter.create_documents([content])120 embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")121 qdrant = Qdrant.from_documents(122 docs, embeddings,123 location=":memory:",124 collection_name="qa"125 )126 return qdrant127 128 129@st.cache_data130def qa(query, documents_to_search, model_name, api_key):131 retriever = get_retriever(st.session_state.content)132 found_docs = retriever.similarity_search(query, k=documents_to_search)133 llm = get_llm(model_name=model_name, model_temperature=0, api_key=api_key)134 template = \135 """136 You're an experienced copywriter. Answer the question in English. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. 137 Answer the question in a way so that the reader has no more questions. Be concise. Make sure you mention all the important information. You can add additional relevant information from yourself that you think may contribute to the overall understanding. Asses critically the provided context, chat history or own answer.138 Chat History: ``` {chat_history} ``` 139 Context: ``` {context} ```140 Question: ``` {question} ```141 Helpful Answer:142 """143 prompt = ChatPromptTemplate.from_template(template=template)144 145 chat_history = [AIMessage(content=' '.join(st.session_state.content_block_summary))]146 messages = prompt.format_messages(context=found_docs, question=query, chat_history=chat_history)147 response = llm(messages)148 return response.content149 150 151def show_audio_message(message):152 st.write(message)153 content_summary_audio = generate_audio(message)154 st.audio(content_summary_audio)155 156 157def show_question_input():158 def submit_question():159 if len(st.session_state.user_question_widget) != 0:160 st.session_state.question = st.session_state.user_question_widget161 st.session_state.user_question_widget = ''162 else:163 logging.info("empty user question")164 165 st.text_area(label="Ask your question about the content of the page:",166 key='user_question_widget',167 on_change=submit_question)168 st.button("Submit")169 170 def on_question_button(question):171 st.session_state.question = question172 173 with st.expander("Example questions:"):174 for q in st.session_state.content_block_questions:175 st.button(q, on_click=on_question_button, args=[q])176 177 178def get_query_params():179 if 'web_url' not in st.session_state:180 params = st.experimental_get_query_params()181 logging.debug(f"query parameters: {params}")182 if 'web_url' in params:183 web_url = params['web_url'][0]184 if len(web_url) > 0:185 if web_url := is_valid_web_link(web_url):186 st.session_state.web_url = web_url187 188 189def show_header():190 if 'web_url' in st.session_state:191 col1, col2 = st.columns(2)192 col1.caption(f"discussing: {st.session_state.web_url}")193 if 'title' in st.session_state:194 col2.caption(f"{st.session_state.title}")195 196 197def get_random_page():198 return 'https://mailchi.mp/expresso/lightpeak'199 200 201def main() -> None:202 try:203 get_query_params()204 init_session()205 show_header()206 207 if 'web_url' not in st.session_state:208 st.header("Doodle")209 st.image("./assets/doodle-img.jpg")210 description = """\211 Meet 'Doodle,' your shortcut to understanding the web! Got a lengthy article you're eyeing? 212 Just paste the link, and in an instant, Doodle delivers a crisp summary and intriguing questions for you to 213 chew on. Want to go hands-free? Doodle's text-to-speech feature will read it to you! Why the name 'Doodle'? 214 Just as a simple doodle can encapsulate a whole idea, we distill webpages down to their essence!215 """216 st.caption(description)217 st.divider()218 219 web_url = st.text_input(label='Paste your link, e.g. https://expresso.today',220 label_visibility='collapsed',221 placeholder='Paste your link, e.g. https://expresso.today')222 col1, _, _, _, col2 = st.columns(5)223 col1.button("Doodle")224 if col2.button("Random Page"):225 web_url = get_random_page()226 if len(web_url) > 0:227 if web_url := is_valid_web_link(web_url):228 st.session_state.web_url = web_url229 st.experimental_rerun()230 else:231 st.warning(232 "Whoops! That link seems to be doing the vanishing act. Could you give it another shot? Magic words: 'Valid Link, Please!' ๐ช")233 234 elif 'content' not in st.session_state:235 with st.spinner(f"reading the web page '{st.session_state.web_url}' ..."):236 st.session_state.web_page = scrape_the_article(st.session_state.web_url)237 st.session_state.title = st.session_state.web_page['title']238 st.session_state.content = st.session_state.web_page['content']239 st.experimental_rerun()240 241 elif 'content_summary' not in st.session_state:242 content_summary = get_content_summary(content=st.session_state.content, model_name="gpt-3.5-turbo-16k",243 api_key=open_api_key)244 st.session_state.content_summary = content_summary['summary']245 st.session_state.content_block_summary = [s['block_summary'] for s in content_summary['blocks']]246 st.session_state.content_block_questions = [s['block_question'] for s in content_summary['blocks']]247 248 show_audio_message(st.session_state.content_summary)249 show_question_input()250 elif 'question' in st.session_state and st.session_state.question is not None:251 question = st.session_state.question252 st.subheader(question)253 st.divider()254 with st.spinner(f'answering the question...'):255 answer = qa(query=question, documents_to_search=20, model_name='gpt-4', api_key=open_api_key)256 show_audio_message(answer)257 st.session_state.question = None258 show_question_input()259 else:260 show_question_input()261 except Exception as e:262 logging.error(e)263 st.warning("""\264 Whoops, looks like a hiccup in the system! But no worries, our tech wizards are already on 265 the case, working their magic. In the meantime, how about giving it another shot?266 """)267 if st.button("Give It Another Go!"):268 st.experimental_rerun()269 270 271if __name__ == "__main__":272 main()273 274# TODO:275# - connect to langsmith276# - chat history277# - store history externaly along with audio description and return from cache278 