Views enable us to actually look at the data,
not only filter, sort, organize and compute statistics on it (what tables are for π’).
Letβs again consider the following data for self-proclaimed β10xβ engineers on the team,
which is the same data we used when explaining tables:Open in your console
Copy
Ask AI
import unifyimport randomfrom datetime import datetime, timedeltanum_employees = 20ages = [ random.randint(20, 50) for _ in range(num_employees)]catchphrases = random.choices( [ "hello... friend", "hell no", "did you ask o3?", "will do it tomorrow", "ask the intern", ], k=num_employees,)last_logins = [ (datetime.now() - timedelta( days=random.randint(0, 5) )).isoformat() for _ in range(num_employees)]open_task_progress = [ { task: random.random() for task in random.sample( [ "add NextJS loader", "SQL migration", "refactor life", ], random.randint(0, 3), ) } for _ in range(num_employees)]for age, catchphrase, last_login, otp in zip( ages, catchphrases, last_logins, open_task_progress): unify.log( age=age, how_10x=random.random()*10, catchphrase=catchphrase, last_login=last_login, open_task_progress=otp, will_to_live=random.choice([True, False]), )
A view tile must be paired with exactly one table, this can be done from the menu at the top.
Every cell in a table is an atomic unit, which can be viewed independently.In itβs most basic form, the view pane acts as an expressive viewer for whatever cell(s) are selected in the table.These cells can be part of the same row, different rows, different columns, or any combination.
In all cases, all of the data will be shown in the view pane.
Columns can be hidden by directly clicking the (-) icon which appears on hover,
and also via the show / hide selector menu at the top.
Column hiding in the view pane is totally independent from hidden columns in the table,
making it easy to split the data across the two formats.For example, consider the following logged data:Open in your console
Copy
Ask AI
import unifyimport randomfor question in ["what is 1 + 1?", "what is 2 + 2?", "what is 3 + 3?"]: student_answer = f"the answer is {random.randint(0, 6)}" correct_marks_to_award = int(int(student_answer[-1]) == int(eval(question[-4:-1]))) awarded_marks = random.randint(0, 1) rationale = f"the student answered {student_answer} " "and I gave them {awarded_marks} marks because I'm not " "good at maths, and {awarded_marks} is my favourite number" diff = correct_marks_to_award - awarded_marks error = abs(diff) unify.log( question, student_answer=student_answer, available_marks=1, awarded_marks=awarded_marks, rationale=rationale, correct_marks_to_award=correct_marks_to_award, diff=diff, error=error, )
The most intuitive way to view this data is to show the numeric data in the table
(with all the power of sorting, grouping, filtering etc.),
and the text data in the view pane to look at the finer details
(and benefit from all the view pane features).
By default, the view pane will only expand one nest level at a time.
The deeper contents of dicts and lists are lazily loaded as the nest is incrementally expanded.
Pressing βExpand All Childrenβ will expand all children recursively with one click,
and βCollapse All Childrenβ will do the inverse.
Aside from presenting the diffs for individual items,
we can also see how the nested structure of dictionaries and lists change across logs.
The same is also true for traces (more details below).
Last but not least, no observability tool would be complete without expressive traces.
Traces enable you to get a complete view of nested function calls from your program π
The trace viewer is shown on the left, which shows the nested structure of your entire trace.
Each item in the trace is a span (unit of computation),
and the children of each span are shown nested underneath.Each span shows the cumulative values for:
Cost: Can be manually specified during tracing for arbitrary programs.
Unifyβs LLM clients automatically populate this based on the cost of the LLM calls.
Tokens: Can be manually specified during tracing when integrating with other LLM providers.
Unifyβs LLM clients automatically populate this based on the tokens used in the LLM calls.
Runtime: Tracked automatically during tracing of the program.
Each span can be folded and unfolded,
and the runtime of the base log can be viewed in the runtime viewer.
The span viewer is shown on the right, and for each span, this shows:
Inputs: All inputs to the function call (if any).
Outputs: All outputs from the function call (if any).
Code: The source code of the function call.
Execution Time: The time taken to execute the function call.
Cost: The cost of the function call, including and excluding cached LLM calls which didnβt cost anything during the trace itself, but would if there was no cache to read from.
ID: The unique identifier for the span.
Expand
click image to maximize
Some extra formatting is done for LLM calls which follow OpenAIβs standard: