trulens-dashboard 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trulens_dashboard-1.0.1/PKG-INFO +42 -0
- trulens_dashboard-1.0.1/README.md +9 -0
- trulens_dashboard-1.0.1/pyproject.toml +47 -0
- trulens_dashboard-1.0.1/trulens/dashboard/Leaderboard.py +312 -0
- trulens_dashboard-1.0.1/trulens/dashboard/__init__.py +13 -0
- trulens_dashboard-1.0.1/trulens/dashboard/appui.py +432 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/__init__.py +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/__init__.py +80 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/dist/assets/SourceCodePro-Bold-839f42df.ttf +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/dist/assets/SourceCodePro-Regular-6e5dd049.ttf +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/dist/assets/SourceCodePro-SemiBold-ccfba566.ttf +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/dist/assets/index-00b8e16d.css +1 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/dist/assets/index-923e1c7a.js +236 -0
- trulens_dashboard-1.0.1/trulens/dashboard/components/record_viewer/dist/index.html +14 -0
- trulens_dashboard-1.0.1/trulens/dashboard/display.py +76 -0
- trulens_dashboard-1.0.1/trulens/dashboard/notebook_utils.py +58 -0
- trulens_dashboard-1.0.1/trulens/dashboard/pages/Evaluations.py +599 -0
- trulens_dashboard-1.0.1/trulens/dashboard/pages/__init__.py +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/py.typed +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/run.py +306 -0
- trulens_dashboard-1.0.1/trulens/dashboard/streamlit.py +249 -0
- trulens_dashboard-1.0.1/trulens/dashboard/streamlit_utils.py +32 -0
- trulens_dashboard-1.0.1/trulens/dashboard/ux/__init__.py +0 -0
- trulens_dashboard-1.0.1/trulens/dashboard/ux/components.py +302 -0
- trulens_dashboard-1.0.1/trulens/dashboard/ux/page_config.py +72 -0
- trulens_dashboard-1.0.1/trulens/dashboard/ux/styles.py +150 -0
- trulens_dashboard-1.0.1/trulens/dashboard/ux/trulens_logo.svg +44 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: trulens-dashboard
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Library to systematically track and evaluate LLM based applications.
|
|
5
|
+
Home-page: https://trulens.org/
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Snowflake Inc.
|
|
8
|
+
Author-email: ml-observability-wg-dl@snowflake.com
|
|
9
|
+
Requires-Python: >=3.9, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Dist: ipywidgets (>=8.1.3,<9.0.0)
|
|
19
|
+
Requires-Dist: jupyter (>=1,<2)
|
|
20
|
+
Requires-Dist: pandas (>=2.1,<3.0)
|
|
21
|
+
Requires-Dist: psutil (>=5.9,<6.0)
|
|
22
|
+
Requires-Dist: rich (>=13.6,<14.0)
|
|
23
|
+
Requires-Dist: streamlit (>=1.33,<2.0)
|
|
24
|
+
Requires-Dist: streamlit-aggrid (==0.3.4)
|
|
25
|
+
Requires-Dist: streamlit-extras (>=0.4,<0.5)
|
|
26
|
+
Requires-Dist: streamlit-pills (>=0.3,<0.4)
|
|
27
|
+
Requires-Dist: traitlets (>=5.0.5,<6.0.0)
|
|
28
|
+
Requires-Dist: trulens-core (>=1.0.0,<2.0.0)
|
|
29
|
+
Project-URL: Documentation, https://trulens.org/trulens/getting_started/
|
|
30
|
+
Project-URL: Repository, https://github.com/truera/trulens
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# trulens-dashboard
|
|
34
|
+
|
|
35
|
+
## Install Node.js
|
|
36
|
+
|
|
37
|
+
See [the Node.js website](https://nodejs.org/en/download/package-manager) for details
|
|
38
|
+
|
|
39
|
+
## Building
|
|
40
|
+
|
|
41
|
+
Build with `python -m build`
|
|
42
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "poetry.core.masonry.api"
|
|
3
|
+
requires = [
|
|
4
|
+
"poetry-core",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
[tool.poetry]
|
|
8
|
+
name = "trulens-dashboard"
|
|
9
|
+
version = "1.0.1"
|
|
10
|
+
description = "Library to systematically track and evaluate LLM based applications."
|
|
11
|
+
authors = [
|
|
12
|
+
"Snowflake Inc. <ml-observability-wg-dl@snowflake.com>",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
packages = [
|
|
17
|
+
{ include = "trulens" },
|
|
18
|
+
]
|
|
19
|
+
homepage = "https://trulens.org/"
|
|
20
|
+
documentation = "https://trulens.org/trulens/getting_started/"
|
|
21
|
+
repository = "https://github.com/truera/trulens"
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Development Status :: 3 - Alpha",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.poetry.dependencies]
|
|
33
|
+
python = "^3.9,!=3.9.7"
|
|
34
|
+
trulens-core = { version = "^1.0.0" }
|
|
35
|
+
ipywidgets = "^8.1.3"
|
|
36
|
+
jupyter = "^1"
|
|
37
|
+
pandas = "^2.1"
|
|
38
|
+
psutil = "^5.9"
|
|
39
|
+
rich = "^13.6"
|
|
40
|
+
streamlit = "^1.33"
|
|
41
|
+
streamlit-aggrid = "==0.3.4"
|
|
42
|
+
streamlit-extras = "^0.4"
|
|
43
|
+
streamlit-pills = "^0.3"
|
|
44
|
+
traitlets = "^5.0.5"
|
|
45
|
+
|
|
46
|
+
[tool.poetry.group.dev.dependencies]
|
|
47
|
+
trulens-core = { path = "../core" }
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from streamlit_extras.switch_page_button import switch_page
|
|
7
|
+
from trulens.core import TruSession
|
|
8
|
+
from trulens.core.database.legacy.migration import MIGRATION_UNKNOWN_STR
|
|
9
|
+
from trulens.core.utils.text import format_quantity
|
|
10
|
+
from trulens.dashboard.streamlit_utils import init_from_args
|
|
11
|
+
from trulens.dashboard.ux import styles
|
|
12
|
+
from trulens.dashboard.ux.components import draw_metadata_and_tags
|
|
13
|
+
from trulens.dashboard.ux.page_config import set_page_config
|
|
14
|
+
from trulens.dashboard.ux.styles import CATEGORY
|
|
15
|
+
|
|
16
|
+
# https://github.com/jerryjliu/llama_index/issues/7244:
|
|
17
|
+
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
# If not imported, gets args from command line and creates Tru singleton
|
|
21
|
+
init_from_args()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def leaderboard():
|
|
25
|
+
"""Render the leaderboard page."""
|
|
26
|
+
|
|
27
|
+
set_page_config(page_title="Leaderboard")
|
|
28
|
+
|
|
29
|
+
session = TruSession() # get singleton whether this file was imported or executed from command line.
|
|
30
|
+
|
|
31
|
+
lms = session.connector.db
|
|
32
|
+
|
|
33
|
+
# Set the title
|
|
34
|
+
st.title("App Leaderboard")
|
|
35
|
+
|
|
36
|
+
def get_data():
|
|
37
|
+
return lms.get_records_and_feedback([])
|
|
38
|
+
|
|
39
|
+
def get_apps():
|
|
40
|
+
return list(lms.get_apps())
|
|
41
|
+
|
|
42
|
+
# sort apps by name
|
|
43
|
+
def sort_selected_apps(selected_apps, records):
|
|
44
|
+
"""
|
|
45
|
+
Sorts the selected_apps list based on the concatenation of app_name and app_version.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
selected_apps (list): List of app_ids to be sorted.
|
|
49
|
+
apps (DataFrame): DataFrame containing app_id and app_json columns.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
list: Sorted list of selected_apps.
|
|
53
|
+
"""
|
|
54
|
+
# Create a mapping from app_id to (app_name, app_version)
|
|
55
|
+
app_info = (
|
|
56
|
+
records.set_index("app_id")["app_json"]
|
|
57
|
+
.apply(
|
|
58
|
+
lambda x: (
|
|
59
|
+
json.loads(x).get("app_name"),
|
|
60
|
+
json.loads(x).get("app_version"),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
.to_dict()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Sort selected_apps based on the concatenation of app_name and app_version
|
|
67
|
+
selected_apps = [
|
|
68
|
+
app_id for app_id in selected_apps if app_id in app_info
|
|
69
|
+
]
|
|
70
|
+
sorted_apps = sorted(
|
|
71
|
+
selected_apps,
|
|
72
|
+
key=lambda app_id: f"{app_info[app_id][0]}{app_info[app_id][1]}",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return sorted_apps
|
|
76
|
+
|
|
77
|
+
records, feedback_col_names = get_data()
|
|
78
|
+
records = records.sort_values(by="app_id")
|
|
79
|
+
|
|
80
|
+
apps = get_apps()
|
|
81
|
+
app_names = sorted(list(set(app["app_name"] for app in apps)))
|
|
82
|
+
|
|
83
|
+
selected_app_names = st.multiselect("Filter apps:", app_names, app_names)
|
|
84
|
+
selected_apps = [
|
|
85
|
+
app["app_id"]
|
|
86
|
+
for app in apps
|
|
87
|
+
if any(name in app["app_name"] for name in selected_app_names)
|
|
88
|
+
]
|
|
89
|
+
st.session_state.app = selected_apps
|
|
90
|
+
|
|
91
|
+
# Filter app versions to only include those from selected apps
|
|
92
|
+
filtered_apps = [app for app in apps if app["app_id"] in selected_apps]
|
|
93
|
+
app_name_versions = sorted(
|
|
94
|
+
list(
|
|
95
|
+
set(
|
|
96
|
+
f"{app['app_name']} - {app['app_version']}"
|
|
97
|
+
for app in filtered_apps
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
selected_app_name_versions = st.multiselect(
|
|
103
|
+
"Filter by app version:", app_name_versions, app_name_versions
|
|
104
|
+
)
|
|
105
|
+
selected_apps = [
|
|
106
|
+
app["app_id"]
|
|
107
|
+
for app in apps
|
|
108
|
+
if f"{app['app_name']} - {app['app_version']}"
|
|
109
|
+
in selected_app_name_versions
|
|
110
|
+
]
|
|
111
|
+
st.session_state.app = selected_apps
|
|
112
|
+
|
|
113
|
+
with st.expander("Advanced Filters"):
|
|
114
|
+
# get tag options
|
|
115
|
+
tags = []
|
|
116
|
+
for i in range(len(apps)):
|
|
117
|
+
tags.append(apps[i]["tags"])
|
|
118
|
+
unique_tags = sorted(list(set(tags)))
|
|
119
|
+
# select tags
|
|
120
|
+
selected_tags = st.multiselect("Filter tags:", unique_tags, unique_tags)
|
|
121
|
+
|
|
122
|
+
# filter to apps with selected tags
|
|
123
|
+
tag_selected_apps = [
|
|
124
|
+
app["app_id"]
|
|
125
|
+
for app in apps
|
|
126
|
+
if any(tag in app["tags"] for tag in selected_tags)
|
|
127
|
+
]
|
|
128
|
+
selected_apps = list(set(selected_apps) & set(tag_selected_apps))
|
|
129
|
+
st.session_state.app = selected_apps
|
|
130
|
+
|
|
131
|
+
# get metadata options
|
|
132
|
+
metadata_keys_unique = set()
|
|
133
|
+
for app in apps:
|
|
134
|
+
metadata_keys_unique.update(app["metadata"].keys())
|
|
135
|
+
metadata_keys_unique = list(metadata_keys_unique)
|
|
136
|
+
# select metadata
|
|
137
|
+
metadata_options = {}
|
|
138
|
+
for metadata_key in metadata_keys_unique:
|
|
139
|
+
unique_values = set()
|
|
140
|
+
for i in range(len(apps)):
|
|
141
|
+
try:
|
|
142
|
+
unique_values.add(apps[i]["metadata"][metadata_key])
|
|
143
|
+
except (KeyError, TypeError):
|
|
144
|
+
pass
|
|
145
|
+
metadata_options[metadata_key] = list(unique_values)
|
|
146
|
+
|
|
147
|
+
# select metadata
|
|
148
|
+
metadata_selections = metadata_options.copy()
|
|
149
|
+
for metadata_key in metadata_options.keys():
|
|
150
|
+
metadata_selections[metadata_key] = st.multiselect(
|
|
151
|
+
"Filter " + metadata_key + ":",
|
|
152
|
+
sorted(metadata_options[metadata_key]),
|
|
153
|
+
sorted(metadata_options[metadata_key]),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# filter to apps with selected metadata
|
|
157
|
+
metadata_selected_apps = [
|
|
158
|
+
app["app_id"]
|
|
159
|
+
for app in apps
|
|
160
|
+
if all(
|
|
161
|
+
metadata_key not in app["metadata"]
|
|
162
|
+
or app["metadata"][metadata_key]
|
|
163
|
+
in metadata_selections[metadata_key]
|
|
164
|
+
for metadata_key in metadata_selections.keys()
|
|
165
|
+
)
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
selected_apps = list(set(selected_apps) & set(metadata_selected_apps))
|
|
169
|
+
st.session_state.app = selected_apps
|
|
170
|
+
|
|
171
|
+
feedback_defs = lms.get_feedback_defs()
|
|
172
|
+
feedback_directions = {
|
|
173
|
+
(
|
|
174
|
+
row.feedback_json.get("supplied_name", "")
|
|
175
|
+
or row.feedback_json["implementation"]["name"]
|
|
176
|
+
): row.feedback_json.get("higher_is_better", True)
|
|
177
|
+
for _, row in feedback_defs.iterrows()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if records.empty:
|
|
181
|
+
st.write("No records yet...")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if records.empty:
|
|
185
|
+
st.write("No records yet...")
|
|
186
|
+
|
|
187
|
+
st.markdown("""---""")
|
|
188
|
+
|
|
189
|
+
selected_apps = sort_selected_apps(selected_apps, records)
|
|
190
|
+
|
|
191
|
+
for app in selected_apps:
|
|
192
|
+
app_df = records.loc[records.app_id == app]
|
|
193
|
+
if app_df.empty:
|
|
194
|
+
continue
|
|
195
|
+
app_str = app_df["app_json"].iloc[0]
|
|
196
|
+
app_json = json.loads(app_str)
|
|
197
|
+
app_name = app_json["app_name"]
|
|
198
|
+
app_version = app_json["app_version"]
|
|
199
|
+
app_name_version = f"{app_name} - {app_version}"
|
|
200
|
+
metadata = app_json.get("metadata")
|
|
201
|
+
tags = app_json.get("tags")
|
|
202
|
+
# st.text('Metadata' + str(metadata))
|
|
203
|
+
st.header(app_name_version, help=draw_metadata_and_tags(metadata, tags))
|
|
204
|
+
app_feedback_col_names = [
|
|
205
|
+
col_name
|
|
206
|
+
for col_name in feedback_col_names
|
|
207
|
+
if not app_df[col_name].isna().all()
|
|
208
|
+
]
|
|
209
|
+
col1, col2, col3, col4, *feedback_cols, col99 = st.columns(
|
|
210
|
+
5 + len(app_feedback_col_names)
|
|
211
|
+
)
|
|
212
|
+
latency_mean = (
|
|
213
|
+
app_df["latency"]
|
|
214
|
+
.apply(lambda td: td if td != MIGRATION_UNKNOWN_STR else None)
|
|
215
|
+
.mean()
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
col1.metric("Records", len(app_df))
|
|
219
|
+
col2.metric(
|
|
220
|
+
"Average Latency (Seconds)",
|
|
221
|
+
(
|
|
222
|
+
f"{format_quantity(round(latency_mean, 5), precision=2)}"
|
|
223
|
+
if not math.isnan(latency_mean)
|
|
224
|
+
else "nan"
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Filter for Snowflake credits costs
|
|
229
|
+
filtered_snowflake_credits_app_df = app_df[
|
|
230
|
+
app_df["cost_currency"] == "Snowflake credits"
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
# Calculate the total cost for Snowflake credits
|
|
234
|
+
if len(filtered_snowflake_credits_app_df) > 0:
|
|
235
|
+
total_cost_snowflake_credits = (
|
|
236
|
+
filtered_snowflake_credits_app_df["total_cost"].dropna().sum()
|
|
237
|
+
)
|
|
238
|
+
col3.metric(
|
|
239
|
+
"Total Cost (Snowflake credits)",
|
|
240
|
+
f"{format_quantity(round(total_cost_snowflake_credits, 8), precision=5)}",
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
col3.empty()
|
|
244
|
+
# Filter for USD costs
|
|
245
|
+
filtered_usd_app_df = app_df[app_df["cost_currency"] == "USD"]
|
|
246
|
+
|
|
247
|
+
if len(filtered_usd_app_df) > 0:
|
|
248
|
+
# Calculate the total cost for USD
|
|
249
|
+
total_cost_usd = filtered_usd_app_df["total_cost"].dropna().sum()
|
|
250
|
+
col3.metric(
|
|
251
|
+
"Total Cost (USD)",
|
|
252
|
+
f"${format_quantity(round(total_cost_usd, 5), precision=2)}",
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
col3.empty()
|
|
256
|
+
|
|
257
|
+
col4.metric(
|
|
258
|
+
"Total Tokens",
|
|
259
|
+
format_quantity(
|
|
260
|
+
sum(
|
|
261
|
+
tokens
|
|
262
|
+
for tokens in app_df.total_tokens
|
|
263
|
+
if tokens is not None
|
|
264
|
+
),
|
|
265
|
+
precision=2,
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
for i, col_name in enumerate(app_feedback_col_names):
|
|
270
|
+
mean = app_df[col_name].mean()
|
|
271
|
+
|
|
272
|
+
st.write(
|
|
273
|
+
styles.stmetricdelta_hidearrow,
|
|
274
|
+
unsafe_allow_html=True,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
higher_is_better = feedback_directions.get(col_name, True)
|
|
278
|
+
|
|
279
|
+
if "distance" in col_name:
|
|
280
|
+
feedback_cols[i].metric(
|
|
281
|
+
label=col_name,
|
|
282
|
+
value=f"{round(mean, 2)}",
|
|
283
|
+
delta_color="normal",
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
cat = CATEGORY.of_score(mean, higher_is_better=higher_is_better)
|
|
287
|
+
feedback_cols[i].metric(
|
|
288
|
+
label=col_name,
|
|
289
|
+
value=f"{round(mean, 2)}",
|
|
290
|
+
delta=f"{cat.icon} {cat.adjective}",
|
|
291
|
+
delta_color=(
|
|
292
|
+
"normal"
|
|
293
|
+
if cat.compare(
|
|
294
|
+
mean, CATEGORY.PASS[cat.direction].threshold
|
|
295
|
+
)
|
|
296
|
+
else "inverse"
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
with col99:
|
|
301
|
+
if st.button("Select App", key=f"app-selector-{app}"):
|
|
302
|
+
st.session_state.app = app
|
|
303
|
+
switch_page("Evaluations")
|
|
304
|
+
|
|
305
|
+
# with st.expander("Model metadata"):
|
|
306
|
+
# st.markdown(draw_metadata(metadata))
|
|
307
|
+
|
|
308
|
+
st.markdown("""---""")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
leaderboard()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from importlib.metadata import version
|
|
2
|
+
|
|
3
|
+
from trulens.core.utils.imports import safe_importlib_package_name
|
|
4
|
+
from trulens.dashboard.run import run_dashboard
|
|
5
|
+
from trulens.dashboard.run import stop_dashboard
|
|
6
|
+
|
|
7
|
+
__version__ = version(safe_importlib_package_name(__package__ or __name__))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"run_dashboard",
|
|
12
|
+
"stop_dashboard",
|
|
13
|
+
]
|