diff --git a/good_ai/src/good_ai/good_ai/helper/__init__.py b/good_ai/src/good_ai/good_ai/helper/__init__.py index e84cb5d..32e14ef 100644 --- a/good_ai/src/good_ai/good_ai/helper/__init__.py +++ b/good_ai/src/good_ai/good_ai/helper/__init__.py @@ -1,2 +1,3 @@ from .get_args import get_args from .snake_case_to_text import snake_case_to_text +from .text_to_hex_color import text_to_hex_color diff --git a/good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py b/good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py new file mode 100644 index 0000000..e906a0c --- /dev/null +++ b/good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py @@ -0,0 +1,13 @@ +import colorsys +from hashlib import md5 + + +def text_to_hex_color(text: str) -> str: + ascii_bytes = text.encode("ascii") + digest = md5( + ascii_bytes + ).hexdigest() # the built-in hash function is salted differently in each process + integer = int(digest, 16) + hue = integer % 6311 / 6311.0 + rgb = colorsys.hsv_to_rgb(hue, 0.75, 0.6) + return "#" + "".join("%02X" % round(i * 255) for i in rgb) diff --git a/good_ai/src/good_ai/good_ai/metrics/assets/github.png b/good_ai/src/good_ai/good_ai/metrics/assets/github.png new file mode 100644 index 0000000..ea6ff54 Binary files /dev/null and b/good_ai/src/good_ai/good_ai/metrics/assets/github.png differ diff --git a/good_ai/src/good_ai/good_ai/metrics/assets/index.css b/good_ai/src/good_ai/good_ai/metrics/assets/index.css index f5469de..5e48b88 100644 --- a/good_ai/src/good_ai/good_ai/metrics/assets/index.css +++ b/good_ai/src/good_ai/good_ai/metrics/assets/index.css @@ -1,3 +1,112 @@ +:root { + --accent-color: #47c2d0; + --error-color: #a30808; + --background-color: #edf5f6; + --small-padding: 10px; + --medium-padding: 20px; + --large-padding: 40px; + --border-radius: 10px; + --shadow: 0 4px 6px -1px rgb(0 0 0 / 10%), 0 2px 4px -1px rgb(0 0 0 / 6%); +} + +* { + margin: 0; + box-sizing: border-box; +} + body { - background-color: hotpink; -} \ No newline at end of file + background-color: var(--background-color); + font-family: Arial, Helvetica, sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: var(--medium-padding) 0 var(--small-padding) 0; +} + +.glance, +.table-container, +.parallel-coords { + padding: var(--large-padding); + margin: var(--large-padding); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + background-color: white; + overflow: hidden; +} + +.glance { + display: flex; +} + +.glance .description { + width: 350px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.glance .dash-graph { + flex: 1; +} + +.table-container { + padding: 0; +} + +.table-container h2 { + padding: var(--small-padding); + margin: 0; +} + +.table-container h2, +footer.watermark { + opacity: 0.35; +} + +.dash-spreadsheet { + overflow: auto; +} + +th, +td { + max-width: 350px; +} + +.parallel-coords { + padding: 0; +} + +footer.watermark { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--large-padding); + background-color: #ddd; + position: relative; +} + +footer.watermark div { + display: flex; + flex-direction: column; +} + +h6 { + font-size: 3rem; + margin-top: 0; +} + +a img { + width: 64px; + height: 64px; + cursor: pointer; + transition: transform 300ms; +} + +a img:hover { + transform: scale(1.1); +} diff --git a/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py b/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py index f214e3d..24c12d9 100644 --- a/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py +++ b/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py @@ -10,13 +10,16 @@ from flask import Flask from good_ai.utilities.unique import unique from ..context import get_context -from ..helper import snake_case_to_text +from ..helper import snake_case_to_text, text_to_hex_color from ..views import SortBy from .get_description import get_description from .get_filter_from_datatable import get_filter_from_datatable +from .get_footer import get_footer def create_dash_app(function_name: str) -> Flask: + accent_color = text_to_hex_color(function_name) + flask_app = Flask(__name__) app = Dash( function_name, @@ -32,24 +35,37 @@ def create_dash_app(function_name: str) -> Flask: documents = get_context().persistence.get_documents() df = pd.DataFrame(documents) + execution_time_histogram = dcc.Graph(config={"displaylogo": False}) + table = dash_table.DataTable( + columns=[{"name": i, "id": i} for i in df.columns], + page_current=0, + page_size=20, + page_action="custom", + filter_action="custom", + filter_query="", + sort_action="custom", + sort_mode="multi", + sort_by=[ + {"column_id": "created", "direction": "desc"}, + ], + ) + app.layout = html.Div( [ - get_description(function_name), html.Div( - table := dash_table.DataTable( - columns=[{"name": i, "id": i} for i in df.columns], - page_current=0, - page_size=20, - page_action="custom", - filter_action="custom", - filter_query="", - sort_action="custom", - sort_mode="multi", - sort_by=[], - ), + [ + get_description( + function_name=function_name, accent_color=accent_color + ), + execution_time_histogram, + ], + className="glance", ), - execution_time_histogram := dcc.Graph(), - parallel_coords := dcc.Graph(), + html.Div([html.H2("Latest traces"), table], className="table-container"), + parallel_coords := dcc.Graph( + className="parallel-coords", config={"displaylogo": False} + ), + get_footer(), interval := dcc.Interval( interval=4 * 1000, # in milliseconds n_intervals=0, @@ -100,15 +116,23 @@ def create_dash_app(function_name: str) -> Flask: ) df = pd.DataFrame(rows) - return px.histogram( + fig = px.histogram( df, x="execution_time_ms", labels={"execution_time_ms": "Execution time (ms)"}, nbins=20, - title="Execution times", + height=400, log_y=True, + color_discrete_sequence=[accent_color], ) + fig.update_layout( + autosize=True, + margin=dict(l=0, r=0, b=0, t=0, pad=0), + ) + + return fig + @app.callback( Output(parallel_coords, "figure"), Input(table, "filter_query"), @@ -131,7 +155,8 @@ def create_dash_app(function_name: str) -> Flask: get_dimension_descriptor(df, c) for c in df.columns if not c.startswith("arg:") and c not in {"id", "created"} - ] + ], + line_color=accent_color, ) ) diff --git a/good_ai/src/good_ai/good_ai/metrics/get_description.py b/good_ai/src/good_ai/good_ai/metrics/get_description.py index 8976963..4059688 100644 --- a/good_ai/src/good_ai/good_ai/metrics/get_description.py +++ b/good_ai/src/good_ai/good_ai/metrics/get_description.py @@ -1,12 +1,11 @@ -from dash.dcc import Markdown +from dash import dcc, html from ..helper import snake_case_to_text -def get_description(function_name: str) -> Markdown: +def get_description(function_name: str, accent_color: str) -> html.Div: markdown_text = f""" - # {snake_case_to_text(function_name)} - metrics - > A human-friendly framework for robust end-to-end AI deployments + View the live data of your deployments here. ## Using the API @@ -14,7 +13,15 @@ def get_description(function_name: str) -> Markdown: ## Metrics - The recent traces and aggregated metrics are presented below. + Recent traces and aggregated metrics are presented below. Try filtering the table. """ - return Markdown(markdown_text) + return html.Div( + [ + html.H1( + f"{snake_case_to_text(function_name)} - metrics", + style={"color": accent_color}, + ), + dcc.Markdown(markdown_text, className="description"), + ] + ) diff --git a/good_ai/src/good_ai/good_ai/metrics/get_footer.py b/good_ai/src/good_ai/good_ai/metrics/get_footer.py new file mode 100644 index 0000000..d7239a3 --- /dev/null +++ b/good_ai/src/good_ai/good_ai/metrics/get_footer.py @@ -0,0 +1,22 @@ +from dash import html + + +def get_footer() -> html.Footer: + return html.Footer( + [ + html.Div( + [ + html.H6("GoodAI"), + html.P( + "A human-friendly framework for robust end-to-end AI deployments." + ), + ] + ), + html.A( + html.Img(src="/assets/github.png"), + href="https://github.com/ScoutinScience/good-ai", + target="_blank", + ), + ], + className="watermark", + ) diff --git a/good_ai/src/good_ai/good_ai/views/query.py b/good_ai/src/good_ai/good_ai/views/query.py index e91cbec..e72b187 100644 --- a/good_ai/src/good_ai/good_ai/views/query.py +++ b/good_ai/src/good_ai/good_ai/views/query.py @@ -11,3 +11,17 @@ class Query(BaseModel): sort: List[SortBy] = [] skip: int = 0 take: int = 100 + + class Config: + schema_extra = { + "example": { + "filter": [ + {"property": "execution_time_ms", "operator": ">", "value": 100} + ], + "sort": [ + {"column_id": "execution_time_ms", "direction": "asc"}, + {"column_id": "id", "direction": "desc"}, + ], + "take": 10, + } + }