├── 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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------