By default, all logs within a project are stored in the default context (represented by the empty string "").

unify.log(x=0, y=1, z=2)

When no context is specified in the interface or the table, then the default context is used. However, you can group your logs into non-overlapping contexts, which can then be displayed in separate tables.

names = ["Zoe", "John", "Jane", "Jim", "Jill"]
qnas = {
    "what is 1 + 1?": 2,
    "what is 2 * 3?": 6,
    "what is 3 - 2?": 1,
    "what is 4 / 2?": 2,
    "what is 5 % 2?": 1
}
regions = ["US", "EU", "UK", "AU", "CA"]

for _ in range(20):
    question, answer = random.choice(
        list(qnas.items())
    )
    unify.log(
        name=random.choice(names),
        question=question,
        answer=answer,
        region=random.choice(regions),
        context="Traffic"
)

predictions = [
    "six", "one", "two", "three", "four", "five"
]

for _ in range(15):
    question, answer = random.choice(
        list(qnas.items())
    )
    unify.log(
        name=random.choice(names),
        question=question,
        prediction=random.choice(
            predictions
        ),
        answer=answer,
        context="Evals"
    )

Context Handling

You can use with unify.Context("..."): to create a context, and all logging inside this block will adopt this context.

with unify.Context("Traffic"):
    unify.log(
        name="Zoe",
        question="what is 1 + 1?",
        region="US"
    )
    unify.log(
        name="John",
        question="what is 2 + 2?",
        region="EU"
    )

IMG

These can also be arbitrarily nested, where each context string is joined via / to define the full context path.

with unify.Context("Sciences"):
    with unify.Context("Maths"):
        unify.log(
            name="Zoe",
            question="what is 1 + 1?",
            region="US"
        )
    with unify.Context("Physics"):
        unify.log(
            name="John",
            question="what is the speed of light?",
            region="EU"
        )

IMG

By default, unify.Context("...") also changes the behaviour of get_logs, returning only the logs relevant within the context.

with unify.Context("Sciences"):
    with unify.Context("Maths"):
        unify.log(
            name="Zoe",
            question="what is 1 + 1?",
            region="US"
        )
        assert len(unify.get_logs()) == 1
    with unify.Context("Physics"):
        unify.log(
            name="John",
            question="what is the speed of light?",
            region="EU"
        )
        assert len(unify.get_logs()) == 1
assert len(unify.get_logs()) == 2

This behaviour can be change by setting unify.Context("...", mode="read") which will only set the context for log getting, and unify.Context("...", mode="write") which will only set the context for log setting.

This makes it easy to quickly get/set different logs for different purposes throughout your codebase, without needing to manage contexts via local variables.

Shared Logs

Logs can be added to as many contexts as you’d like, without duplication.

exam_results = [
    {"name": "Zoe", "passed": False},
    {"name": "John", "passed": True}
]
with unify.Context("Results"):
    logs = unify.create_logs(entries=exam_results)
with unify.Context("Failed"):
    unify.add_logs_to_context(
        [l.id for l in logs if not l.entries["passed"]]
    )

If any logs are then updated inplace, they will be reflected in all contexts.

[l.update(entries={"passed": True}) for l in logs]

GIF

Note that in this case, Results and Failed are disconnected, and both sets of logs cannot be viewed in a single table.

To organize data in a single table, you can use column contexts, more on those soon!