├── tools ├── __init__.py ├── calculator_tools.py ├── search_tools.py └── browser_tools.py ├── requirements.txt ├── .gitignore ├── images ├── beach.png ├── agent_steps.png └── Your paragraph text (1).png ├── secrets.example ├── LICENSE ├── trip_tasks.py ├── README.md ├── streamlit_app.py └── trip_agents.py /tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | crewai 2 | streamlit 3 | openai 4 | unstructured 5 | pyowm 6 | tools -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | __pycache__ 4 | agent 5 | .streamlit/secrets.toml 6 | .venv -------------------------------------------------------------------------------- /images/beach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonykipkemboi/trip_planner_agent/HEAD/images/beach.png -------------------------------------------------------------------------------- /images/agent_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonykipkemboi/trip_planner_agent/HEAD/images/agent_steps.png -------------------------------------------------------------------------------- /images/Your paragraph text (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonykipkemboi/trip_planner_agent/HEAD/images/Your paragraph text (1).png -------------------------------------------------------------------------------- /secrets.example: -------------------------------------------------------------------------------- 1 | SERPER_API_KEY="API_KEY_HERE" # https://serper.dev/ (free tier) 2 | BROWSERLESS_API_KEY="API_KEY_HERE" # https://www.browserless.io/ (free tier) 3 | OPENAI_API_KEY="API_KEY_HERE" -------------------------------------------------------------------------------- /tools/calculator_tools.py: -------------------------------------------------------------------------------- 1 | from langchain.tools import tool 2 | 3 | 4 | class CalculatorTools(): 5 | 6 | @tool("Make a calcualtion") 7 | def calculate(operation): 8 | """Useful to perform any mathematical calculations, 9 | like sum, minus, multiplication, division, etc. 10 | The input to this tool should be a mathematical 11 | expression, a couple examples are `200*7` or `5000/2*10` 12 | """ 13 | return eval(operation) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tony Kipkemboi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/search_tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | import streamlit as st 5 | from langchain.tools import tool 6 | 7 | 8 | class SearchTools(): 9 | 10 | @tool("Search the internet") 11 | def search_internet(query): 12 | """Useful to search the internet 13 | about a a given topic and return relevant results""" 14 | top_result_to_return = 4 15 | url = "https://google.serper.dev/search" 16 | payload = json.dumps({"q": query}) 17 | headers = { 18 | 'X-API-KEY': st.secrets['SERPER_API_KEY'], 19 | 'content-type': 'application/json' 20 | } 21 | response = requests.request("POST", url, headers=headers, data=payload) 22 | # check if there is an organic key 23 | if 'organic' not in response.json(): 24 | return "Sorry, I couldn't find anything about that, there could be an error with you serper api key." 25 | else: 26 | results = response.json()['organic'] 27 | string = [] 28 | for result in results[:top_result_to_return]: 29 | try: 30 | string.append('\n'.join([ 31 | f"Title: {result['title']}", f"Link: {result['link']}", 32 | f"Snippet: {result['snippet']}", "\n-----------------" 33 | ])) 34 | except KeyError: 35 | next 36 | 37 | return '\n'.join(string) 38 | -------------------------------------------------------------------------------- /tools/browser_tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | import streamlit as st 5 | from crewai import Agent, Task 6 | from langchain.tools import tool 7 | from unstructured.partition.html import partition_html 8 | 9 | 10 | class BrowserTools(): 11 | 12 | @tool("Scrape website content") 13 | def scrape_and_summarize_website(website): 14 | """Useful to scrape and summarize a website content""" 15 | url = f"https://chrome.browserless.io/content?token={st.secrets['BROWSERLESS_API_KEY']}" 16 | payload = json.dumps({"url": website}) 17 | headers = {'cache-control': 'no-cache', 'content-type': 'application/json'} 18 | response = requests.request("POST", url, headers=headers, data=payload) 19 | elements = partition_html(text=response.text) 20 | content = "\n\n".join([str(el) for el in elements]) 21 | content = [content[i:i + 8000] for i in range(0, len(content), 8000)] 22 | summaries = [] 23 | for chunk in content: 24 | agent = Agent( 25 | role='Principal Researcher', 26 | goal= 27 | 'Do amazing researches and summaries based on the content you are working with', 28 | backstory= 29 | "You're a Principal Researcher at a big company and you need to do a research about a given topic.", 30 | allow_delegation=False) 31 | task = Task( 32 | agent=agent, 33 | description= 34 | f'Analyze and summarize the content bellow, make sure to include the most relevant information in the summary, return only the summary nothing else.\n\nCONTENT\n----------\n{chunk}' 35 | ) 36 | summary = task.execute() 37 | summaries.append(summary) 38 | return "\n\n".join(summaries) 39 | -------------------------------------------------------------------------------- /trip_tasks.py: -------------------------------------------------------------------------------- 1 | from crewai import Task 2 | from textwrap import dedent 3 | from datetime import date 4 | 5 | 6 | class TripTasks(): 7 | 8 | def identify_task(self, agent, origin, cities, interests, range): 9 | return Task(description=dedent(f""" 10 | Analyze and select the best city for the trip based 11 | on specific criteria such as weather patterns, seasonal 12 | events, and travel costs. This task involves comparing 13 | multiple cities, considering factors like current weather 14 | conditions, upcoming cultural or seasonal events, and 15 | overall travel expenses. 16 | 17 | Your final answer must be a detailed 18 | report on the chosen city, and everything you found out 19 | about it, including the actual flight costs, weather 20 | forecast and attractions. 21 | {self.__tip_section()} 22 | 23 | Traveling from: {origin} 24 | City Options: {cities} 25 | Trip Date: {range} 26 | Traveler Interests: {interests} 27 | """), 28 | expected_output="A detailed report on the chosen city with flight costs, weather forecast, and attractions.", 29 | agent=agent) 30 | 31 | def gather_task(self, agent, origin, interests, range): 32 | return Task(description=dedent(f""" 33 | As a local expert on this city you must compile an 34 | in-depth guide for someone traveling there and wanting 35 | to have THE BEST trip ever! 36 | Gather information about key attractions, local customs, 37 | special events, and daily activity recommendations. 38 | Find the best spots to go to, the kind of place only a 39 | local would know. 40 | This guide should provide a thorough overview of what 41 | the city has to offer, including hidden gems, cultural 42 | hotspots, must-visit landmarks, weather forecasts, and 43 | high level costs. 44 | 45 | The final answer must be a comprehensive city guide, 46 | rich in cultural insights and practical tips, 47 | tailored to enhance the travel experience. 48 | {self.__tip_section()} 49 | 50 | Trip Date: {range} 51 | Traveling from: {origin} 52 | Traveler Interests: {interests} 53 | """), 54 | expected_output="A comprehensive city guide with cultural insights and practical tips.", 55 | agent=agent) 56 | 57 | def plan_task(self, agent, origin, interests, range): 58 | return Task(description=dedent(f""" 59 | Expand this guide into a full travel 60 | itinerary for this time {range} with detailed per-day plans, including 61 | weather forecasts, places to eat, packing suggestions, 62 | and a budget breakdown. 63 | 64 | You MUST suggest actual places to visit, actual hotels 65 | to stay and actual restaurants to go to. 66 | 67 | This itinerary should cover all aspects of the trip, 68 | from arrival to departure, integrating the city guide 69 | information with practical travel logistics. 70 | 71 | Your final answer MUST be a complete expanded travel plan, 72 | formatted as markdown, encompassing a daily schedule, 73 | anticipated weather conditions, recommended clothing and 74 | items to pack, and a detailed budget, ensuring THE BEST 75 | TRIP EVER, Be specific and give it a reason why you picked 76 | # up each place, what make them special! {self.__tip_section()} 77 | 78 | Trip Date: {range} 79 | Traveling from: {origin} 80 | Traveler Interests: {interests} 81 | """), 82 | expected_output="A complete 7-day travel plan, formatted as markdown, with a daily schedule and budget.", 83 | agent=agent) 84 | 85 | def __tip_section(self): 86 | return "If you do your BEST WORK, I'll tip you $100 and grant you any wish you want!" 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏖️ VacAIgent: Streamlit-Integrated AI Crew for Trip Planning 2 | 3 | _Forked and enhanced from the_ [_crewAI examples repository_](https://github.com/joaomdmoura/crewAI-examples/tree/main/trip_planner) 4 | 5 | ![Beach Vacation Scene ~ generated by GPT-4V](images/beach.png) 6 | 7 | ## Introduction 8 | 9 | VacAIgent leverages the CrewAI framework to automate and enhance the trip planning experience, integrating a user-friendly Streamlit interface. This project demonstrates how autonomous AI agents can collaborate and execute complex tasks efficiently, now with an added layer of interactivity and accessibility through Streamlit. 10 | 11 | **Check out the video below for code walkthrough** 👇 12 | 13 | 14 | Watch the video 15 | 16 | 17 | (_Trip example originally developed by [@joaomdmoura](https://x.com/joaomdmoura)_) 18 | 19 | ## CrewAI Framework 20 | 21 | CrewAI simplifies the orchestration of role-playing AI agents. In VacAIgent, these agents collaboratively decide on cities and craft a complete itinerary for your trip based on specified preferences, all accessible via a streamlined Streamlit user interface. 22 | 23 | ## Streamlit Interface 24 | 25 | The introduction of [Streamlit](https://streamlit.io/) transforms this application into an interactive web app, allowing users to easily input their preferences and receive tailored travel plans. 26 | 27 | ## Running the Application 28 | 29 | To experience the VacAIgent app: 30 | 31 | - **Configure Environment**: Set up the environment variables for [Browseless](https://www.browserless.io/), [Serper](https://serper.dev/), and [OpenAI](https://openai.com/). Use the `secrets.example` as a guide to add your keys then move that file (`secrets.toml`) to `.streamlit/secrets.toml`. 32 | 33 | - **Install Dependencies**: Execute `pip install -r requirements.txt` in your terminal. 34 | - **Launch the App**: Run `streamlit run streamlit_app.py` to start the Streamlit interface. 35 | 36 | ★ **Disclaimer**: The application uses GPT-4 by default. Ensure you have access to OpenAI's API and be aware of the associated costs. 37 | 38 | ## Details & Explanation 39 | 40 | - **Streamlit UI**: The Streamlit interface is implemented in `streamlit_app.py`, where users can input their trip details. 41 | - **Components**: 42 | - `./trip_tasks.py`: Contains task prompts for the agents. 43 | - `./trip_agents.py`: Manages the creation of agents. 44 | - `./tools directory`: Houses tool classes used by agents. 45 | - `./streamlit_app.py`: The heart of the Streamlit app. 46 | 47 | ## Using GPT 3.5 48 | 49 | To switch from GPT-4 to GPT-3.5, pass the llm argument in the agent constructor: 50 | 51 | ```python 52 | from langchain.chat_models import ChatOpenAI 53 | 54 | llm = ChatOpenAI(model='gpt-3.5-turbo') # Loading gpt-3.5-turbo (see more OpenAI models at https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4) 55 | 56 | class TripAgents: 57 | # ... existing methods 58 | 59 | def local_expert(self): 60 | return Agent( 61 | role='Local Expert', 62 | goal='Provide insights about the selected city', 63 | tools=[SearchTools.search_internet, BrowserTools.scrape_and_summarize_website], 64 | llm=llm, 65 | verbose=True 66 | ) 67 | 68 | ``` 69 | 70 | ## Using Local Models with Ollama 71 | 72 | For enhanced privacy and customization, you can integrate local models like Ollama: 73 | 74 | ### Setting Up Ollama 75 | 76 | - **Installation**: Follow Ollama's guide for installation. 77 | - **Configuration**: Customize the model as per your requirements. 78 | 79 | ### Integrating Ollama with CrewAI 80 | 81 | Pass the Ollama model to agents in the CrewAI framework: 82 | 83 | ```python 84 | from langchain.llms import Ollama 85 | 86 | ollama_model = Ollama(model="agent") 87 | 88 | class TripAgents: 89 | # ... existing methods 90 | 91 | def local_expert(self): 92 | return Agent( 93 | role='Local Expert', 94 | tools=[SearchTools.search_internet, BrowserTools.scrape_and_summarize_website], 95 | llm=ollama_model, 96 | verbose=True 97 | ) 98 | 99 | ``` 100 | 101 | ## Benefits of Local Models 102 | 103 | - **Privacy**: Process sensitive data in-house. 104 | - **Customization**: Tailor models to fit specific needs. 105 | - **Performance**: Potentially faster responses with on-premises models. 106 | 107 | ## License 108 | 109 | VacAIgent is open-sourced under the MIT License. 110 | -------------------------------------------------------------------------------- /streamlit_app.py: -------------------------------------------------------------------------------- 1 | from crewai import Crew 2 | from trip_agents import TripAgents, StreamToExpander 3 | from trip_tasks import TripTasks 4 | import streamlit as st 5 | import datetime 6 | import sys 7 | 8 | st.set_page_config(page_icon="✈️", layout="wide") 9 | 10 | 11 | def icon(emoji: str): 12 | """Shows an emoji as a Notion-style page icon.""" 13 | st.write( 14 | f'{emoji}', 15 | unsafe_allow_html=True, 16 | ) 17 | 18 | 19 | class TripCrew: 20 | 21 | def __init__(self, origin, cities, date_range, interests): 22 | self.cities = cities 23 | self.origin = origin 24 | self.interests = interests 25 | self.date_range = date_range 26 | self.output_placeholder = st.empty() 27 | 28 | def run(self): 29 | agents = TripAgents() 30 | tasks = TripTasks() 31 | 32 | city_selector_agent = agents.city_selection_agent() 33 | local_expert_agent = agents.local_expert() 34 | travel_concierge_agent = agents.travel_concierge() 35 | 36 | identify_task = tasks.identify_task( 37 | city_selector_agent, 38 | self.origin, 39 | self.cities, 40 | self.interests, 41 | self.date_range 42 | ) 43 | 44 | gather_task = tasks.gather_task( 45 | local_expert_agent, 46 | self.origin, 47 | self.interests, 48 | self.date_range 49 | ) 50 | 51 | plan_task = tasks.plan_task( 52 | travel_concierge_agent, 53 | self.origin, 54 | self.interests, 55 | self.date_range 56 | ) 57 | 58 | crew = Crew( 59 | agents=[ 60 | city_selector_agent, local_expert_agent, travel_concierge_agent 61 | ], 62 | tasks=[identify_task, gather_task, plan_task], 63 | verbose=True 64 | ) 65 | 66 | result = crew.kickoff() 67 | self.output_placeholder.markdown(result) 68 | 69 | return result 70 | 71 | 72 | if __name__ == "__main__": 73 | icon("🏖️ VacAIgent") 74 | 75 | st.subheader("Let AI agents plan your next vacation!", 76 | divider="rainbow", anchor=False) 77 | 78 | import datetime 79 | 80 | today = datetime.datetime.now().date() 81 | next_year = today.year + 1 82 | jan_16_next_year = datetime.date(next_year, 1, 10) 83 | 84 | with st.sidebar: 85 | st.header("👇 Enter your trip details") 86 | with st.form("my_form"): 87 | location = st.text_input( 88 | "Where are you currently located?", placeholder="San Mateo, CA") 89 | cities = st.text_input( 90 | "City and country are you interested in vacationing at?", placeholder="Bali, Indonesia") 91 | date_range = st.date_input( 92 | "Date range you are interested in traveling?", 93 | min_value=today, 94 | value=(today, jan_16_next_year + datetime.timedelta(days=6)), 95 | format="MM/DD/YYYY", 96 | ) 97 | interests = st.text_area("High level interests and hobbies or extra details about your trip?", 98 | placeholder="2 adults who love swimming, dancing, hiking, and eating") 99 | 100 | submitted = st.form_submit_button("Submit") 101 | 102 | st.divider() 103 | 104 | # Credits to joaomdmoura/CrewAI for the code: https://github.com/joaomdmoura/crewAI 105 | st.sidebar.markdown( 106 | """ 107 | Credits to [**@joaomdmoura**](https://twitter.com/joaomdmoura) 108 | for creating **crewAI** 🚀 109 | """, 110 | unsafe_allow_html=True 111 | ) 112 | 113 | st.sidebar.info("Click the logo to visit GitHub repo", icon="👇") 114 | st.sidebar.markdown( 115 | """ 116 | 117 | CrewAI Logo 118 | 119 | """, 120 | unsafe_allow_html=True 121 | ) 122 | 123 | 124 | if submitted: 125 | with st.status("🤖 **Agents at work...**", state="running", expanded=True) as status: 126 | with st.container(height=500, border=False): 127 | sys.stdout = StreamToExpander(st) 128 | trip_crew = TripCrew(location, cities, date_range, interests) 129 | result = trip_crew.run() 130 | status.update(label="✅ Trip Plan Ready!", 131 | state="complete", expanded=False) 132 | 133 | st.subheader("Here is your Trip Plan", anchor=False, divider="rainbow") 134 | st.markdown(result) 135 | -------------------------------------------------------------------------------- /trip_agents.py: -------------------------------------------------------------------------------- 1 | from crewai import Agent 2 | import re 3 | import streamlit as st 4 | from langchain_community.llms import OpenAI 5 | 6 | from tools.browser_tools import BrowserTools 7 | from tools.calculator_tools import CalculatorTools 8 | from tools.search_tools import SearchTools 9 | 10 | ## My initial parsing code using callback handler to print to app 11 | # def streamlit_callback(step_output): 12 | # # This function will be called after each step of the agent's execution 13 | # st.markdown("---") 14 | # for step in step_output: 15 | # if isinstance(step, tuple) and len(step) == 2: 16 | # action, observation = step 17 | # if isinstance(action, dict) and "tool" in action and "tool_input" in action and "log" in action: 18 | # st.markdown(f"# Action") 19 | # st.markdown(f"**Tool:** {action['tool']}") 20 | # st.markdown(f"**Tool Input** {action['tool_input']}") 21 | # st.markdown(f"**Log:** {action['log']}") 22 | # st.markdown(f"**Action:** {action['Action']}") 23 | # st.markdown( 24 | # f"**Action Input:** ```json\n{action['tool_input']}\n```") 25 | # elif isinstance(action, str): 26 | # st.markdown(f"**Action:** {action}") 27 | # else: 28 | # st.markdown(f"**Action:** {str(action)}") 29 | 30 | # st.markdown(f"**Observation**") 31 | # if isinstance(observation, str): 32 | # observation_lines = observation.split('\n') 33 | # for line in observation_lines: 34 | # if line.startswith('Title: '): 35 | # st.markdown(f"**Title:** {line[7:]}") 36 | # elif line.startswith('Link: '): 37 | # st.markdown(f"**Link:** {line[6:]}") 38 | # elif line.startswith('Snippet: '): 39 | # st.markdown(f"**Snippet:** {line[9:]}") 40 | # elif line.startswith('-'): 41 | # st.markdown(line) 42 | # else: 43 | # st.markdown(line) 44 | # else: 45 | # st.markdown(str(observation)) 46 | # else: 47 | # st.markdown(step) 48 | 49 | 50 | class TripAgents(): 51 | 52 | def city_selection_agent(self): 53 | return Agent( 54 | role='City Selection Expert', 55 | goal='Select the best city based on weather, season, and prices', 56 | backstory='An expert in analyzing travel data to pick ideal destinations', 57 | tools=[ 58 | SearchTools.search_internet, 59 | BrowserTools.scrape_and_summarize_website, 60 | ], 61 | verbose=True, 62 | # step_callback=streamlit_callback, 63 | ) 64 | 65 | def local_expert(self): 66 | return Agent( 67 | role='Local Expert at this city', 68 | goal='Provide the BEST insights about the selected city', 69 | backstory="""A knowledgeable local guide with extensive information 70 | about the city, it's attractions and customs""", 71 | tools=[ 72 | SearchTools.search_internet, 73 | BrowserTools.scrape_and_summarize_website, 74 | ], 75 | verbose=True, 76 | # step_callback=streamlit_callback, 77 | ) 78 | 79 | def travel_concierge(self): 80 | return Agent( 81 | role='Amazing Travel Concierge', 82 | goal="""Create the most amazing travel itineraries with budget and 83 | packing suggestions for the city""", 84 | backstory="""Specialist in travel planning and logistics with 85 | decades of experience""", 86 | tools=[ 87 | SearchTools.search_internet, 88 | BrowserTools.scrape_and_summarize_website, 89 | CalculatorTools.calculate, 90 | ], 91 | verbose=True, 92 | # step_callback=streamlit_callback, 93 | ) 94 | 95 | ########################################################################################### 96 | # Print agent process to Streamlit app container # 97 | # This portion of the code is adapted from @AbubakrChan; thank you! # 98 | # https://github.com/AbubakrChan/crewai-UI-business-product-launch/blob/main/main.py#L210 # 99 | ########################################################################################### 100 | class StreamToExpander: 101 | def __init__(self, expander): 102 | self.expander = expander 103 | self.buffer = [] 104 | self.colors = ['red', 'green', 'blue', 'orange'] # Define a list of colors 105 | self.color_index = 0 # Initialize color index 106 | 107 | def write(self, data): 108 | # Filter out ANSI escape codes using a regular expression 109 | cleaned_data = re.sub(r'\x1B\[[0-9;]*[mK]', '', data) 110 | 111 | # Check if the data contains 'task' information 112 | task_match_object = re.search(r'\"task\"\s*:\s*\"(.*?)\"', cleaned_data, re.IGNORECASE) 113 | task_match_input = re.search(r'task\s*:\s*([^\n]*)', cleaned_data, re.IGNORECASE) 114 | task_value = None 115 | if task_match_object: 116 | task_value = task_match_object.group(1) 117 | elif task_match_input: 118 | task_value = task_match_input.group(1).strip() 119 | 120 | if task_value: 121 | st.toast(":robot_face: " + task_value) 122 | 123 | # Check if the text contains the specified phrase and apply color 124 | if "Entering new CrewAgentExecutor chain" in cleaned_data: 125 | # Apply different color and switch color index 126 | self.color_index = (self.color_index + 1) % len(self.colors) # Increment color index and wrap around if necessary 127 | 128 | cleaned_data = cleaned_data.replace("Entering new CrewAgentExecutor chain", f":{self.colors[self.color_index]}[Entering new CrewAgentExecutor chain]") 129 | 130 | if "City Selection Expert" in cleaned_data: 131 | # Apply different color 132 | cleaned_data = cleaned_data.replace("City Selection Expert", f":{self.colors[self.color_index]}[City Selection Expert]") 133 | if "Local Expert at this city" in cleaned_data: 134 | cleaned_data = cleaned_data.replace("Local Expert at this city", f":{self.colors[self.color_index]}[Local Expert at this city]") 135 | if "Amazing Travel Concierge" in cleaned_data: 136 | cleaned_data = cleaned_data.replace("Amazing Travel Concierge", f":{self.colors[self.color_index]}[Amazing Travel Concierge]") 137 | if "Finished chain." in cleaned_data: 138 | cleaned_data = cleaned_data.replace("Finished chain.", f":{self.colors[self.color_index]}[Finished chain.]") 139 | 140 | self.buffer.append(cleaned_data) 141 | if "\n" in data: 142 | self.expander.markdown(''.join(self.buffer), unsafe_allow_html=True) 143 | self.buffer = [] 144 | --------------------------------------------------------------------------------