# βš–οΈ Dataset Diffs Source: https://docs.oxen.ai/concepts/diffs Quickly find changes in your datasets with Oxen.ai Oxen.ai has built in tools to help you find differences in your datasets. It is as simple as running the `oxen diff` command with the path to your datasets. ```bash CLI theme={null} oxen diff dataset.csv -o diff.csv ``` ```bash Python theme={null} import oxen result = oxen.diff("dataset.csv") print(result.get()) # .get() will resolve the diff data type ``` ``` Column changes: + label (str) Row changes: Ξ” 1 (modified) + 3 (added) - 2 (removed) shape: (6, 7) +-------------+-----+-----+-------+--------+-------------+-------------------+ | file | x | y | width | height | label.right | .oxen.diff.status | | --- | --- | --- | --- | --- | --- | --- | | str | i64 | i64 | i64 | i64 | str | str | +-------------+-----+-----+-------+--------+-------------+-------------------+ | image_0.jpg | 0 | 0 | 10 | 10 | cat | modified | | image_1.jpg | 1 | 2 | 10 | 20 | null | removed | | image_1.jpg | 200 | 100 | 10 | 20 | dog | added | | image_2.jpg | 4 | 10 | 20 | 20 | null | removed | | image_3.jpg | 4 | 10 | 20 | 20 | dog | added | | image_4.jpg | 10 | 10 | 10 | 10 | dog | added | +-------------+-----+-----+-------+--------+-------------+-------------------+ ``` Under the hood Oxen.ai is using a combination of hashing and diffing algorithms to find the differences in your datasets. This allows you to quickly find changes in your datasets, whether they are rows, columns, or individual cells. Oxen's diff tool tries to strike a balance between being easy to use and being flexible enough to handle complex datasets. # Diff Types Oxen.ai currently supports a [TextDiff](/python-api/diff/text_diff) and a [TabularDiff](/python-api/diff/tabular_diff) data type. The `TabularDiff` data type is used to represent the differences in tabular data, such as CSV, TSV, or Parquet files. The `TextDiff` data type is used to represent the differences in text files, such as markdown, code, or configuration files. In the future, we plan to add support for other data types such as images, audio, and video. # Diff Command Syntax The `oxen diff` command supports multiple syntax patterns similar to Git: * `oxen diff` - Compare working tree with HEAD * `oxen diff ` - Compare specific file in working tree with HEAD * `oxen diff ` - Compare commit with HEAD * `oxen diff ..` - Compare two commits using range syntax * `oxen diff ` - Compare two commits * `oxen diff [--] [...]` - Compare commit with HEAD for specific paths * `oxen diff .. [--] [...]` - Compare two commits for specific paths * `oxen diff file1 file2` - Compare two local files (not in repository) The `..` separator can be used to specify commit ranges, making it easy to compare different versions of your data. # Pick Your Tooling All the functionality below is available through the [πŸ–₯️ Command Line](/getting-started/command-line/start_repository), [πŸ¦€ Rust Library](https://crates.io/crates/liboxen), [🐍 Python Library](/python-api), as well as the [🌎 Web Interface](https://oxen.ai). This guide will focus on the command line tooling, but the same principles apply to the other interfaces. Using the [Oxen.ai Hub](https://oxen.ai) you can quickly visualize and navigate the changes in your datasets with an easy to use interface. Sign up for free πŸ‘‰ [here](https://oxen.ai/register). Data Diff We will build up from simple examples to more complex ones. Starting from adding and removing rows, to modifying rows, to detecting schema changes, and finally providing specific target fields you are interested in. All the data below can be found in the [datasets/diff-examples repository](https://www.oxen.ai/datasets/diff-examples). # Let's Build a Dataset In order to demonstrate how to use the `oxen diff` command, we will need a dataset to work with. Imagine we are collecting a dataset for fine-tuning a Large Language Model (LLM). This dataset will have a set of `prompts` and a `category` that they belong to. Create a new file called `dataset.csv` and add the following data to it. ``` prompt,category What is the capital of France?,geography What is 2+10?,math What is the capital of Germany?,geography What is the best python library for http requests?,programming Tell me a story about an ox.,story ``` If you are not familiar with the `oxen df` command it is a handy tool to manipulate and inspect tabular data. You can use it with any CSV, TSV, Parquet, or line delimited JSON file. ```bash CLI theme={null} oxen df dataset.csv ``` ```python Python theme={null} import oxen # the oxen.df module can save and load files into a polars data frame # for more info on polars see: https://pola.rs/ # it will infer the type of data from the fileextension df = oxen.df("dataset.csv") print(df) ``` ``` shape: (5, 2) +-----------------------------------+-------------+ | prompt | category | | --- | --- | | str | str | +-----------------------------------+-------------+ | What is the capital of France? | geography | | What is 2+10? | math | | What is the capital of Germany? | geography | | What is the best python library … | programming | | Tell me a story about an ox. | story | +-----------------------------------+-------------+ ``` In order to return to this initial version of the data at any point, let's add and commit it to a local Oxen repository. ```bash CLI theme={null} oxen init oxen add dataset.csv oxen commit -m "Initial dataset" ``` ```python Python theme={null} import os from oxen import Repo repo = Repo("/path/to/data") repo.init() repo.add("dataset.csv") repo.commit("Adding my data") ``` ## Adding Rows Let's start with a completely additive workflow as if we are collecting a large datasets of prompts. Add a row to the dataset by simply appending to the file. ```bash theme={null} echo "20*20,math" >> dataset.csv ``` If you want to see the changes between the current version of your file and the previous version, you can use the `oxen diff` command. By default, Oxen compares the working tree with the HEAD commit (the last committed version). ```bash CLI theme={null} oxen diff dataset.csv ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv") print(result.get()) ``` ``` Row changes: + 1 (added) shape: (1, 3) +--------+----------+-------------------+ | prompt | category | .oxen.diff.status | | --- | --- | --- | | str | str | str | +--------+----------+-------------------+ | 20*20 | math | added | +--------+----------+-------------------+ ``` As you can see Oxen found the one added row and augmented the data frame with an `.oxen.diff.status` column to show the status of the row. There are three possible values for the `.oxen.diff.status` column: * `added` * `removed` * `modified` ## Removing Rows Next remove the first entry of the file to see how Oxen handles deletions. We will use the `sed` command with the in place flag `-i` to remove the first row from the file. ```bash theme={null} sed -i '' '2d' dataset.csv ``` (Note: the `-i ''` flag is for MacOS, if you are using Linux you can simply use `-i`.) Since the file is a CSV with a header row, you will need to remove the second row hence `2d`. Verify that the first row was removed by using the `oxen diff` command. ```bash CLI theme={null} oxen diff dataset.csv ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv") print(result.get()) ``` ``` Row changes: + 1 (added) - 1 (removed) shape: (2, 3) +--------------------------------+-----------+-------------------+ | prompt | category | .oxen.diff.status | | --- | --- | --- | | str | str | str | +--------------------------------+-----------+-------------------+ | What is the capital of France? | geography | removed | | 20*20 | math | added | +--------------------------------+-----------+-------------------+ ``` ## Modifing Rows This is great for adding and removing rows, but what about modifying rows? Say we change the `category` of "geography" to be a more generic "trivia" category and add a new prompt to it "What is the fastest land animal?". Edit the `datasets.csv` file to look like this: ```bash theme={null} prompt,category What is 2+10?,math What is the capital of Germany?,trivia What is the best python library for http requests?,programming Tell me a story about an ox.,story 20*20,math What is the fastest land animal?,trivia ``` If we run the `oxen diff` command again, we will see the changes. ``` Row changes: + 3 (added) - 2 (removed) shape: (5, 3) +----------------------------------+-----------+-------------------+ | prompt | category | .oxen.diff.status | | --- | --- | --- | | str | str | str | +----------------------------------+-----------+-------------------+ | What is the capital of France? | geography | removed | | What is the capital of Germany? | geography | removed | | 20*20 | math | added | | What is the capital of Germany? | trivia | added | | What is the fastest land animal? | trivia | added | +----------------------------------+-----------+-------------------+ ``` You'll notice that for every row we modified we end up having +1 addition and +1 removal. This is because Oxen is treating the modified row as one added row and one removed row. ## Specifying Keys The reason that the above example treats the modified row as a new row and a removed row is because both the `prompt` and `category` columns being considered keys under the hood. `oxen diff` hashes the combination of keys in order to find differences in the data. The default keys are all the common columns between the two versions of the datasets. If you have a unique identifier for each row, you can use the `--keys` (or `-k`) flag to specify the column or columns that should be used as the primary keys. ```bash CLI theme={null} oxen diff dataset.csv -k prompt ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv", keys=["prompt"]) print(result.get()) ``` ``` Row changes: Ξ” 1 (modified) + 2 (added) - 1 (removed) shape: (4, 4) +----------------------------------+---------------+----------------+-------------------+ | prompt | category.left | category.right | .oxen.diff.status | | --- | --- | --- | --- | | str | str | str | str | +----------------------------------+---------------+----------------+-------------------+ | 20*20 | null | math | added | | What is the capital of France? | geography | null | removed | | What is the capital of Germany? | geography | trivia | modified | | What is the fastest land animal? | null | trivia | added | +----------------------------------+---------------+----------------+-------------------+ ``` Great! This collapsed our added and removed row into a single modified row. The category column has now been split into two columns, `category.left` and `category.right`, to show the old and new values. Assumming these changes look good, you can add and commit the changes to your local repository. ```bash CLI theme={null} oxen add dataset.csv oxen commit -m "Added and removed rows" ``` ```python Python theme={null} # ... instantiate repo repo.add("dataset.csv") repo.commit("Added and removed rows") ``` ## Adding Columns Adding and removing rows is great, but what about changes to the schema itself? Instead of using the prompt as a key, let's add an `id` column to the dataset and use that as the key. Let's also add an `answer` column to the dataset, so that we can evaluate the responses. Update your raw csv with the new columns like so: ``` id,prompt,answer,category 0,What is 2+10?,12,math 1,What is the capital of Germany?,Berlin,trivia 2,What is the best python library for http requests?,requests,programming 3,Tell me a story about an ox.,I am sorry I cannot do that.,story 4,20*20,400,math 5,What is the fastest land animal?,cheetah,trivia ``` Now if you run the `oxen diff` command, you will see that it automatically detects the added columns and displays the new values in `id.right` and `answer.right`. ```bash CLI theme={null} oxen diff dataset.csv ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv") print(result.get()) ``` ``` Column changes: + id (i64) + answer (str) Row changes: Ξ” 6 (modified) shape: (6, 5) +-----------------------------------+-------------+----------+------------------------------+-------------------+ | prompt | category | id.right | answer.right | .oxen.diff.status | | --- | --- | --- | --- | --- | | str | str | i64 | str | str | +-----------------------------------+-------------+----------+------------------------------+-------------------+ | 20*20 | math | 4 | 400 | modified | | What is 2+10? | math | 0 | 12 | modified | | What is the best python library … | programming | 2 | requests | modified | | Tell me a story about an ox. | story | 3 | I am sorry I cannot do that. | modified | | What is the capital of Germany? | trivia | 1 | Berlin | modified | | What is the fastest land animal? | trivia | 5 | cheetah | modified | +-----------------------------------+-------------+----------+------------------------------+-------------------+ ``` Removing a column would show the values in columns called `.left` to show the values in columns that are now missing. If you are happy with the changes, you can add and commit the changes to your local repository. ```bash CLI theme={null} oxen add dataset.csv oxen commit -m "Added id and answer column" ``` ```python Python theme={null} # ... instantiate repo repo.add("dataset.csv") repo.commit("Added id and answer column") ``` ## Specifying Compares Not only can you specify keys to narrow down the scope of what fields oxen hashes, but you can also specify columns to compare with the `--compares` (`-c`) flag. This specifies the fields oxen compares. You can think of the keys as the fields that are hashed to create a unique id to tell if a row was added or removed. The compares are the fields that are compared to check if a row was modified. By default if you specify a single key, the rest of the columns become the compares. If you specify multiple keys, the compares are all the columns that are not keys. To see this in action, let's add one row, remove one row, and modify 3 existing ones to demonstrate how this works. In this case we will only modify values of the `answer` column. Overwrite the `dataset.csv` file with the following data. ``` id,prompt,answer,category 0,What is 2+10?,12,math 1,What is the capital of Germany?,The capital of Germany is Berlin,trivia 3,Tell me a story about an ox.,I am sorry Hal.,story 4,20*20,20*20=400,math 5,What is the fastest land animal?,cheetah,trivia 6,What is Oxen.ai?,Imagine git - but can handle large datasets,trivia ``` Since we only modified the answers in this dataset and not the category or the prompt, we can use the `-c` flag to specify that we are only interested in changes in the `answer` column. ```bash CLI theme={null} oxen diff dataset.csv -k id,prompt -c answer ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv", keys=["id", "prompt"], compares=["answer"]) print(result.get()) ``` ``` Row changes: Ξ” 3 (modified) + 1 (added) - 1 (removed) shape: (5, 5) +-----+-----------------------------+----------------------------+------------------------+-------------------+ | id | prompt | answer.left | answer.right | .oxen.diff.status | | --- | --- | --- | --- | --- | | i64 | str | str | str | str | +-----+-----------------------------+----------------------------+------------------------+-------------------+ | 1 | What is the capital of | The capital of Germany is | Berlin | modified | | | Germany? | Berlin | | | | 2 | What is the best python | null | requests | added | | | library … | | | | | 3 | Tell me a story about an | I am sorry Hal. | I am sorry I cannot do | modified | | | ox. | | that. | | | 4 | 20*20 | 20*20=400 | 400 | modified | | 6 | What is Oxen.ai? | Imagine git - but can | null | removed | | | | handle lar… | | | +-----+-----------------------------+----------------------------+------------------------+-------------------+ ``` Contrast this with a default diff which will show 8 changes, 4 added and 4 removed, and you can see the id field is duplicated because we are flagging one addition and one removal for each changed row. ```bash CLI theme={null} oxen diff dataset.csv ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv") print(result.get()) ``` ``` Row changes: + 4 (added) - 4 (removed) shape: (8, 5) +-----+----------------------------------+----------------------------------+-------------+-------------------+ | id | prompt | answer | category | .oxen.diff.status | | --- | --- | --- | --- | --- | | i64 | str | str | str | str | +-----+----------------------------------+----------------------------------+-------------+-------------------+ | 1 | What is the capital of Germany? | Berlin | trivia | added | | 1 | What is the capital of Germany? | The capital of Germany is Berlin | trivia | removed | | 2 | What is the best python library | requests | programming | added | | | … | | | | | 3 | Tell me a story about an ox. | I am sorry Hal. | story | removed | | 3 | Tell me a story about an ox. | I am sorry I cannot do that. | story | added | | 4 | 20*20 | 20*20=400 | math | removed | | 4 | 20*20 | 400 | math | added | | 6 | What is Oxen.ai? | Imagine git - but can handle | trivia | removed | | | | lar… | | | +-----+----------------------------------+----------------------------------+-------------+-------------------+ ``` A diff that only specifies a key will show the correct number of changes, but it may have many columns that are not relevant to the changes you are interested in. This is because under the hood Oxen infers the compares to be the remaining columns. Having more control over the compares is where the `-c` flag comes in handy. To see how this works, try using the `-k` flag on the same dataset without any compares. ```bash CLI theme={null} oxen diff dataset.csv -k id ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv", keys=["id"]) print(result.get()) ``` ``` Row changes: Ξ” 3 (modified) + 1 (added) - 1 (removed) shape: (5, 7) +-----+-----------------+-----------------+-----------------+---------------+----------------+----------------+ | id | prompt | answer.left | answer.right | category.left | category.right | .oxen.diff.sta | | --- | --- | --- | --- | --- | --- | tus | | i64 | str | str | str | str | str | --- | | | | | | | | str | +-----+-----------------+-----------------+-----------------+---------------+----------------+----------------+ | 1 | What is the | The capital of | Berlin | trivia | trivia | modified | | | capital of | Germany is | | | | | | | Germany? | Berlin | | | | | | 2 | What is the | null | requests | null | programming | added | | | best python | | | | | | | | library … | | | | | | | 3 | Tell me a story | I am sorry Hal. | I am sorry I | story | story | modified | | | about an ox. | | cannot do that. | | | | | 4 | 20*20 | 20*20=400 | 400 | math | math | modified | | 6 | What is | Imagine git - | null | trivia | null | removed | | | Oxen.ai? | but can handle | | | | | | | | lar… | | | | | +-----+-----------------+-----------------+-----------------+---------------+----------------+----------------+ ``` The above output is `(5 rows x 7 columns)` which isn't too bad, but if you have a dataset with many columns, it can quickly become overwhelming with irrelevant information. If you know where to look, you can use the `-c` flag to narrow down the scope of the diff. ## Saving Results The `--output` (`-o`) flag can be used to save the results of the diff to a new file. This is useful if you want to save the results of the diff to a new file for further inspection or to share with others. ```bash CLI theme={null} oxen diff dataset.csv -o diff.csv ``` ```python Python theme={null} import oxen oxen.diff("dataset.csv", output="diff.csv") ``` The above command will save the results of the diff to a new file called `diff.csv`. You can then load it into a jupyter notebook, pandas, or even back into Oxen to do more analysis on the results. ## Real World Example To drive all these features home, imagine you have taken the dataset above and run it through an LLM with a prompt to get the responses. You have saved the results in a new file called `model_results.csv`. Below is an example script that runs the prompts through `gpt-3.5-turbo` and saves the results to a new file. This script uses the `openai` python package to interact with the OpenAI API. [process\_csv\_with\_openai.py](https://www.oxen.ai/datasets/diff-examples/file/main/process_with_openai.py) ```python theme={null} import csv import time from openai import OpenAI import argparse import os client = OpenAI( # This is the default and can be omitted api_key=os.environ.get("OPENAI_API_KEY"), ) def process_csv_with_gpt4(input_csv, output_csv): print(f'Processing {input_csv} with GPT-4 and writing to {output_csv}') with open(input_csv, mode='r', encoding='utf-8') as infile, open(output_csv, mode='w', newline='', encoding='utf-8') as outfile: reader = csv.DictReader(infile) fieldnames = ['id', 'prompt', 'answer', 'category', 'response', 'is_correct', 'model', 'inference_time'] writer = csv.DictWriter(outfile, fieldnames=fieldnames) writer.writeheader() for row in reader: start_time = time.time() print(f'Processing row: {row}') chat_completion = client.chat.completions.create( messages=[ { "role": "user", "content": row['prompt'], } ], model="gpt-3.5-turbo", ) end_time = time.time() inference_time = end_time - start_time # Simplified correctness check; customize based on your needs print(f'Chat completion: {chat_completion}') response = chat_completion.choices[0].message.content.strip() is_correct = 'yes' if row['answer'].lower() in response.lower() else 'no' writer.writerow({ 'id': row['id'], 'prompt': row['prompt'], 'answer': row['answer'], 'category': row['category'], 'response': response, 'is_correct': is_correct, 'model': 'gpt-3.5-turbo', # Adjust based on the model used 'inference_time': inference_time }) # main if __name__ == '__main__': # argparse can be used to accept input/output file names from command line parser = argparse.ArgumentParser(description='Process CSV with GPT-4') parser.add_argument('input_csv', help='Input CSV file') parser.add_argument('output_csv', help='Output CSV file') args = parser.parse_args() process_csv_with_gpt4(args.input_csv, args.output_csv) ``` Run this script on the `dataset.csv` file to get the `model_results.csv` file. ```bash theme={null} python process_csv_with_openai.py dataset.csv model_results.csv ``` Quickly inspect the `model_results.csv` file with the `oxen df` command to make sure the csv was created correctly. ```bash theme={null} oxen df model_results.csv ``` Output: ``` shape: (6, 8) +-----+--------------------------+------------------------+-------------+--------------------------------+------------+-------+----------------+ | id | prompt | answer | category | response | is_correct | model | inference_time | | --- | --- | --- | --- | --- | --- | --- | --- | | i64 | str | str | str | str | str | str | f64 | +-----+--------------------------+------------------------+-------------+--------------------------------+------------+-------+----------------+ | 0 | What is 2+10? | 12 | math | 2+10=12 | yes | gpt-4 | 0.750142 | | 1 | What is the capital of | Berlin | trivia | Berlin | yes | gpt-4 | 0.428595 | | | Germany? | | | | | | | | 2 | What is the best python | requests | programming | There is no one best library | yes | gpt-4 | 3.663857 | | | library … | | | for… | | | | | 3 | Tell me a story about an | I am sorry I cannot do | story | Once upon a time in a small | no | gpt-4 | 7.15331 | | | ox. | that. | | vill… | | | | | 4 | 20*20 | 400 | math | 400 | yes | gpt-4 | 0.422363 | | 5 | What is the fastest land | cheetah | trivia | The fastest land animal is the | yes | gpt-4 | 0.91197 | | | animal? | | | c… | | | | +-----+--------------------------+------------------------+-------------+--------------------------------+------------+-------+----------------+ ``` This dataset has the same `id`, `prompt`, `answer`, and `category` columns as the original dataset, but it also has some additional columns such as `response`, `is_correct`, `model`, and `inference_time`. Add and commit the model results to your local repository. ```bash theme={null} oxen add model_results.csv oxen commit -m "Added model results" ``` Let's say you tweaked the prompt and wanted to run the dataset through the LLM again. Since you have the results versioned in your local repository, you can fearlessly overwrite the file and run the `oxen diff` command to see the differences. Overwrite the `model_results.csv` file with the new results. ``` id,prompt,answer,category,response,is_correct,model,inference_time 0,What is 2+10?,12,math,12,true,model-2,0.21 1,What is the capital of Germany?,Berlin,trivia,Berlin,true,model-2,0.12 2,What is the best python library for http requests?,requests,programming,requests,true,model-2,0.31 3,Tell me a story about an ox.,I am sorry I cannot do that.,story,I am sorry I cannot do that.,true,model-2,0.23 4,20*20,400,math,400,true,model-2,0.09 5,What is the fastest land animal?,cheetah,trivia,cheetah,true,model-2,0.41 ``` If we do a base diff without any flags, we will see that every row is has been marked as added and removed, since the `model` and `inference_time` columns could be different for each row. ```bash CLI theme={null} oxen diff dataset.csv ``` ```python Python theme={null} import oxen result = oxen.diff("dataset.csv") print(result.get()) ``` ``` Row changes: + 6 (added) - 6 (removed) shape: (12, 9) +-----+----------------------------------+---------+----------+---+------------+---------+----------------+-------------------+ | id | prompt | answer | category | … | is_correct | model | inference_time | .oxen.diff.status | | --- | --- | --- | --- | | --- | --- | --- | --- | | i64 | str | str | str | | bool | str | f64 | str | +-----+----------------------------------+---------+----------+---+------------+---------+----------------+-------------------+ | 4 | 20*20 | 400 | math | … | true | model-2 | 0.09 | added | | 4 | 20*20 | 400 | math | … | true | model-1 | 0.1 | removed | | 0 | What is 2+10? | 12 | math | … | true | model-2 | 0.21 | added | | 0 | What is 2+10? | 12 | math | … | true | model-1 | 0.23 | removed | | … | … | … | … | … | … | … | … | … | | 1 | What is the capital of Germany? | Berlin | trivia | … | true | model-2 | 0.12 | added | | 1 | What is the capital of Germany? | Berlin | trivia | … | false | model-1 | 0.11 | removed | | 5 | What is the fastest land animal? | cheetah | trivia | … | true | model-1 | 0.4 | removed | | 5 | What is the fastest land animal? | cheetah | trivia | … | true | model-2 | 0.41 | added | +-----+----------------------------------+---------+----------+---+------------+---------+----------------+------------ ``` This is clearly not what we want. We want to see the differences in the `response` and `is_correct` columns, and ignore the `model` and `inference_time` columns. In combination with the `--keys` flag, you can use the `--compares` (or `-c`) flag to specify the columns you are interested in. ```bash CLI theme={null} oxen diff model_results.csv -k id,prompt,answer -c response,is_correct ``` ```python Python theme={null} import oxen result = oxen.diff( "dataset.csv", keys=["id", "prompt", "answer"], compares=["response", "is_correct"] ) print(result.get()) ``` ``` Row changes: Ξ” 2 (modified) shape: (2, 8) +-----+----------------+----------------+----------------+----------------+----------------+----------------+---------------+ | id | prompt | answer | response.left | response.right | is_correct.lef | is_correct.rig | .oxen.diff.st | | --- | --- | --- | --- | --- | t | ht | atus | | i64 | str | str | str | str | --- | --- | --- | | | | | | | bool | bool | str | +-----+----------------+----------------+----------------+----------------+----------------+----------------+---------------+ | 1 | What is the | Berlin | Munich | Berlin | false | true | modified | | | capital of | | | | | | | | | Germany? | | | | | | | | 3 | Tell me a | I am sorry I | Once upon a | I am sorry I | false | true | modified | | | story about an | cannot do | time | cannot do | | | | | | ox. | that. | | that. | | | | +-----+----------------+----------------+----------------+----------------+----------------+----------------+---------------+ ``` This now narrows down the scope of the diff to only the `response` and `is_correct` columns. We can see that the new model has a different response for the prompts `1` and `3`. Diff allows us to quickly narrow down the responses that model 1 and model 2 disagree on, and which ones are correct. ## Next Up: Comparing Different Files Now that you understand the basics of the diff command, you may be wondering if you can compare different files or different commits. The answer is yes! ## Comparing Different Files You can compare two local files (that may not be in the repository) by passing both file paths: ```bash CLI theme={null} oxen diff model_results_1.csv model_results_2.csv ``` ```python Python theme={null} import oxen result = oxen.diff("model_results_1.csv", to="model_results_2.csv") print(result.get()) ``` ## Comparing Different Commits You can also compare the same file across different commits or branches using commit identifiers: ```bash CLI theme={null} # Compare a specific commit with HEAD oxen diff abc123 dataset.csv # Compare two commits using range syntax oxen diff abc123..def456 dataset.csv # Compare two commits oxen diff abc123 def456 dataset.csv # Compare branches oxen diff main..feature-branch dataset.csv ``` ```python Python theme={null} import oxen # Compare specific commit with HEAD result = oxen.diff("dataset.csv", revision="abc123") print(result.get()) ``` This is useful for tracking how your dataset has evolved over time, comparing different versions of model results, or understanding what changed between branches. You can find all the example data used in this guide in the [datasets/diff-examples repository](https://www.oxen.ai/datasets/diff-examples). [Next: Comparing Different Files](/concepts/compare) # 🏷️ File Metadata Source: https://docs.oxen.ai/concepts/file_metadata Oxen.ai gives you the flexibility to attach metadata to files to make them more discoverable and useful. ## Data Type Detection By default, Oxen.ai will detect the data type of a file based on the file extension and content type. The default data types are: * `tabular` -> `csv`, `tsv`, `jsonl`, `parquet`, `arrow` * `text` -> `txt` * `image` -> `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff`, `webp` * `video` -> `mp4`, `mov` * `audio` -> `mp3`, `wav`, `m4a`, `ogg`, `flac` ## Tabular Data When you add a tabular file to Oxen, it automatically detects and versions the schema of any tabular data. This is done by using [Polars](https://www.pola.rs/) under the hood to infer the column names and datatypes. To list all the schemas that have been detected and committed, you can use the `oxen schemas` subcommand. ```bash theme={null} oxen schemas ``` Output: ``` +-----------------------+------+----------------------------------+------------------------+ | path | name | hash | fields | +==========================================================================================+ | annotations/train.csv | ? | 53732ea1c2a9ba5807bd59978ebb69f5 | [file, ..., is_fluffy] | |-----------------------+------+----------------------------------+------------------------| | annotations/test.csv | ? | 36d0edc8779f42e30b0d630aa83bc83c | [file, ..., height] | +-----------------------+------+----------------------------------+------------------------+ ``` The schema detection is done on a per file basis. This means that if you have a directory of csv or parquet files, each file will have its own schema. ## View Schema To view a specific schema, you can pass in a schema hash, name, or path to the `oxen schemas` command. ```bash theme={null} oxen schemas annotations/train.csv ``` Output: ``` +--------+-------+----------+ | name | dtype | metadata | +===========================+ | file | str | | |--------+-------+----------| | label | str | | |--------+-------+----------| | min_x | f64 | | |--------+-------+----------| | min_y | f64 | | |--------+-------+----------| | width | i64 | | |--------+-------+----------| | height | i64 | | +--------+-------+----------+ ``` ## Add Schema Schemas are automatically detected when you add `csv`, `tsv`, `jsonl`, `parquet`, and `arrow` files to Oxen. Before a schema is committed, you can see the detected schemas in the `oxen status` command. ```bash theme={null} oxen add annotations/train.csv oxen status ``` Output: ``` On branch main -> 503591398980c485 Directories to be committed added: annotations with 1 file Files to be committed: (use "oxen restore --staged ..." to unstage) modified: annotations/train.csv Schemas to be committed (use "oxen schemas show --staged " to view staged schema) detected schema: annotations/train.csv 23d86a4c1481b817b57ee8ccd7d9016b ``` To view more detailed information about the detected schema, use the `--staged` flag on the `oxen schemas` command. ```bash theme={null} oxen schemas --staged annotations/train.csv ``` Output: ``` annotations/train.csv 23d86a4c1481b817b57ee8ccd7d9016b +-----------+-------+----------+ | name | dtype | metadata | +==============================+ | file | str | | |-----------+-------+----------| | label | str | | |-----------+-------+----------| | min_x | f64 | | |-----------+-------+----------| | min_y | f64 | | |-----------+-------+----------| | width | f64 | | |-----------+-------+----------| | height | f64 | | |-----------+-------+----------| | is_fluffy | str | | |-----------+-------+----------| | breed | str | | +-----------+-------+----------+ ``` To view how Polars interprets the schema before adding the file, you can use the `oxen df` command with the `--schema` flag. ```bash theme={null} oxen df annotations/train.csv --schema ``` Output: ``` +-----------+-------+ | column | dtype | +===================+ | file | str | |-----------+-------| | label | str | |-----------+-------| | min_x | f64 | |-----------+-------| | min_y | f64 | |-----------+-------| | width | f64 | |-----------+-------| | height | f64 | |-----------+-------| | is_fluffy | str | |-----------+-------| | breed | str | +-----------+-------+ ``` ## Additional Metadata You can also add additional information to the schema. This is useful if you want to provide context about the data for a UI, data fetching, or any other reason. Notice the empty column `metadata` in the schema above. You can add arbitrary JSON blobs to the schema itself, as well as each column. Metadata may provide useful information for your end application: * Transforms you want to perform. * How you want to render the data. * Information about the data itself, such as a description of the schema or colun. ## Schema Metadata At the root of each schema is an `Optional` metadata value. This is useful for adding information about the schema itself. For example, you can add a description of the schema or a json blob that gives context to a data renderer. ```bash theme={null} oxen schemas add annotations/train.csv -m '{"task": "bounding_box", "description": "Extracting bounding boxes from images"}' ``` You will see the additional metadata listed above the schema if it is added. ``` "annotations/train.csv" {"task": "bounding_box", "description": "Extracting bounding boxes from images"} +-----------+-------+----------+ | name | dtype | metadata | +==============================+ | file | str | | |-----------+-------+----------| | label | str | | |-----------+-------+----------| | min_x | f64 | | |-----------+-------+----------| | min_y | f64 | | |-----------+-------+----------| | width | f64 | | |-----------+-------+----------| | height | f64 | | |-----------+-------+----------| | is_fluffy | str | | |-----------+-------+----------| | breed | str | | +-----------+-------+----------+ ``` ## Column Metadata You can also add metadata to specific columns. Say you wanted to add information to the `file` column about the root directory of the images, you could do the following: ```bash theme={null} oxen schemas add annotations/train.csv -c 'file' -m '{"root": "images/"}' ``` Output: ``` "annotations/train.csv" +-----------+-------+---------------------+ | name | dtype | metadata | +=========================================+ | file | str | {"root": "images/"} | |-----------+-------+---------------------| | label | str | | |-----------+-------+---------------------| | min_x | f64 | | |-----------+-------+---------------------| | min_y | f64 | | |-----------+-------+---------------------| | width | f64 | | |-----------+-------+---------------------| | height | f64 | | |-----------+-------+---------------------| | is_fluffy | str | | |-----------+-------+---------------------| | breed | str | | +-----------+-------+---------------------+ ``` The `-c` flag stands for `column` and the `-m` flag stands for `metadata`. The metadata is a JSON blob that can be used to store any information you want. The [OxenHub UI](https://oxen.ai) uses schema metadata to render more complex datatypes in the UI. For example viewing inline images directly in a dataframe. OxenHub UI ## Setting Metadata Over HTTP The commands above stage schema changes in a local repository. To do the same from an application, [oxen-server](/getting-started/oxen-server) exposes both levels as endpoints on a [workspace](/concepts/remote-repos). A workspace stages the change against a commit, so metadata can be set without committing first β€” useful for data frames an application edits continuously. Both endpoints **replace** the metadata at their level rather than merging into it. Read the current value from the data frame's schema and merge your keys into it, or you will drop metadata someone else set. ### Schema Metadata `PUT /api/repos/:namespace/:repo_name/workspaces/:workspace_id/data_frames/schema/:path` ```bash theme={null} curl -X PUT \ "https://hub.oxen.ai/api/repos/your-username/my-dataset/workspaces/my-workspace/data_frames/schema/annotations/train.csv" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"metadata": {"task": "bounding_box", "description": "Extracting bounding boxes from images"}}' ``` Output: ``` {"status":"success","status_message":"resource_updated","oxen_version":"0.52.4"} ``` This is the HTTP equivalent of `oxen schemas add -m '{...}'`. ### Column Metadata `POST /api/repos/:namespace/:repo_name/workspaces/:workspace_id/data_frames/columns/schema/metadata/:path` The column is named in the body rather than the path, alongside its metadata. ```bash theme={null} curl -X POST \ "https://hub.oxen.ai/api/repos/your-username/my-dataset/workspaces/my-workspace/data_frames/columns/schema/metadata/annotations/train.csv" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"column_name": "file", "metadata": {"_oxen": {"render": {"func": "image"}}}}' ``` Output: ``` {"status":"success","status_message":"resource_updated","oxen_version":"0.52.4"} ``` This is the HTTP equivalent of `oxen schemas add -c 'file' -m '{...}'`, and the example above is what `--render image` writes. ### Reading It Back Metadata set this way comes back on the data frame's schema, at `data_frame.source.schema.metadata` for the schema itself and on each entry of `data_frame.source.schema.fields` for a column. ```bash theme={null} curl "https://hub.oxen.ai/api/repos/your-username/my-dataset/workspaces/my-workspace/data_frames/resource/annotations/train.csv" \ -H "Authorization: Bearer YOUR_API_KEY" ``` The `_oxen` key is reserved for metadata oxen itself understands, such as the `render` functions described below. Your own keys can sit alongside it. ## Commit The Schema Schemas changes will not be saved until you commit them. To view the schemas staged for commit, you can use the `--staged` flag. ```bash theme={null} oxen schemas --staged ``` Output: ``` +-----------------------+------+----------------------------------+--------------------+ | path | name | hash | fields | +======================================================================================+ | annotations/train.csv | ? | 9d1fac486f95120403d7f18232fa5520 | [file, ..., breed] | +-----------------------+------+----------------------------------+--------------------+ ``` You can then commit the schema to the dataframe with the `commit` subcommand. ```bash theme={null} oxen commit -m "Overriding schema for annotations/train.csv" ``` These changes are persistent across commits and will be carried forward. ## Name Schema It is nice to have human readable names to refer to schemas by. Use the `oxen schemas name` command to name a schema. ```bash theme={null} oxen schemas name annotations/train.csv bounding_box ``` ## Remove Schema If you have accidentally staged a schema, you can remove it with the `oxen schemas rm` command. ```bash theme={null} oxen schemas rm annotations/train.csv --staged ``` ## Render Images Oxen.ai can render images through the webhub if you add the proper schema metadata. ```bash theme={null} oxen schemas add data.csv -c 'file' --render image ``` Under the hood this applied a metadata blob to the column, telling Oxen to render an image. More verbosely it would look like: ```bash theme={null} oxen schemas add data.csv -c 'file' -m '{ "_oxen": { "render": { "func": "image" } } }' ``` ## Render Links Oxen.ai can render links to other files through the webhub if you add the proper schema metadata. ```bash theme={null} oxen schemas add data.csv -c 'file' --render link ``` Under the hood this applied a metadata blob to the column, telling Oxen to render an link. More verbosely it would look like: ```bash theme={null} oxen schemas add data.csv -c 'file' -m '{ "_oxen": { "render": { "func": "link" } } }' ``` # πŸ“Š Datasets Source: https://docs.oxen.ai/examples/data/datasets In Oxen.ai, datasets are used to structure your unstructured data. Any tabular data file will automatically be turned into a collaborative database that can be versioned and used to organize data, can be queried, or fed in as training data for models. Oxen.ai datasets are accessible with an easy to use web interface as well as a command line tools, python, and HTTP library. ## Repositories vs Datasets Repositories are the top level container for your datasets. They are a collection of versioned files and directories. Oxen.ai Repo Certain files within Oxen with the extensions `csv`, `tsv`, `jsonl` and `parquet` gain super powers. These dataset files can be multi-modal containing links to images, audio, and PDFs. You can query them in natural language, and edit them like a spreadsheet. Image Net Under the hood we turn these raw files into a lightweight database that can be queried, edited, versioned, and downloaded. ## View Your Dataset Click the file your repository to open the dataset you want to work with. If you want to follow along with this example, download the [Thinking LLMs](https://www.oxen.ai/ox/Thinking-LLMs) dataset. Oxen.ai Thinking LLMs Repo ## Download Your Dataset Datasets can be downloaded directly from the UI or using the CLI and Python library. You can grab any revision of the dataset by specifying the revision as a branch name or commit id. ```python Python theme={null} from oxen import RemoteRepo # Connect to the remote repository (does not download any data) repo = RemoteRepo("YOUR_USERNAME/YOUR_REPO") # Download the dataset repo.download("path/to/dataset.jsonl", revision="main") ``` ```bash CLI theme={null} oxen download YOUR_USERNAME/YOUR_REPO path/to/dataset.jsonl --revision main ``` ## Upload Your Dataset Once you have created a repository, you can use the "Add Files" button on your repository to upload dataset files through the UI. The dataset will automatically be versioned so you can iterate on it and track changes. Oxen.ai Thinking LLMs Repo Datasets can also be uploaded from the command line or python library. This allows you to integrate Oxen into your existing codebases or CI/CD pipelines. The workflow is similar to git where you `add` and `commit` your changes. ```python Python theme={null} from oxen import RemoteRepo # Connect to the remote repository repo = RemoteRepo("YOUR_USERNAME/YOUR_REPO") # Upload the file to the remote repository repo.add("path/to/dataset.jsonl", dst="datasets/") # Commit the changes repo.commit("Add dataset.jsonl to the datasets/ directory") ``` ```bash CLI theme={null} # Clone the repository oxen clone https://hub.oxen.ai/YOUR_USERNAME/YOUR_REPO cd YOUR_REPO # Add the file to the repository oxen add path/to/dataset.jsonl # Commit the changes oxen commit -m "Add path/to/dataset.jsonl" # Sync the changes to the remote repository oxen push ``` To perform **write operations** on datasets, you need to be an editor on the repository and have your username and API key set. You can set your username and API key using the [CLI](/getting-started/command-line/setup) or [Python library](/python-api/index). Read more about [Authentication & Authorization](/getting-started/auth). ## Using `fsspec` Since datasets are just stored as files and directories, you can interact with them directly using [fsspec](https://filesystem-spec.readthedocs.io/en/latest/). This allows you to read and write to them similar to how you would with a local file system. For example if you want to read the contents of a file on the server, you can simply use the `open` method. ```python Python theme={null} import oxen fs = oxen.OxenFS("YOUR_USERNAME", "YOUR_REPO") with fs.open("path/to/dataset.jsonl") as f: content = f.read() # Print the first 100 characters of the file print(content[:100]) ``` If you want to write to a file, you can use the `write` method. ```python Python theme={null} fs = oxen.OxenFS("YOUR_USERNAME", "YOUR_REPO") with fs.open("path/to/dataset.jsonl", "w") as f: f.write("Hello, world!") ``` If you want to specify a commit message, simply add the commit message in the scope of the `with` block. ```python Python theme={null} fs = oxen.OxenFS("YOUR_USERNAME", "YOUR_REPO") with fs.open("path/to/dataset.jsonl", "w") as f: f.write("{\"question\": \"What is the capital of France?\", \"answer\": \"Paris\"}") f.commit_message = "Add a new row" ``` To learn more about how to use fsspec with Oxen, check out the [OxenFS](/python-api/oxen_fs) documentation. ## Using Pandas Since `OxenFS` implements the `fsspec` interface, you can use it with any library that supports `fsspec`. For example, you can use it with [pandas](https://pandas.pydata.org/) to read and write to datasets. ```python Python theme={null} import pandas as pd # Format: oxen:///@/ df = pd.read_parquet("oxen://openai:gsm8k@main/gsm8k_test.parquet") # Print the first 5 rows print(df.head()) # Apply a transformation to the dataframe df["answer"] = df["answer"].apply(lambda x: x.upper()) # Write the dataframe to a new file df.to_parquet("oxen://openai:gsm8k@main/gsm8k_test_new.parquet") ``` ## Your Dataset is a Database Datasets look like raw files on the surface, but some of their superpowers come from the fact that Oxen.ai can index them into a [DuckDB](https://duckdb.org/) database on the remote server. This allows you to query your dataset directly with SQL. You can use the [DataFrame](/python-api/data_frame) class in the python library to interact with your dataset as a database. ```python Python theme={null} from oxen import DataFrame # Connect to and index the data frame df = DataFrame("YOUR_USERNAME/YOUR_REPO", "path/to/file.jsonl") # Print the first 5 rows that are spam results = df.query("SELECT * FROM df where category = 'spam' LIMIT 5") print(results) ``` Not only can you query your dataset, but you can also add rows, add columns, and perform other database operations before committing your changes. This is useful if you want to build labeling workflows or other data pipelines. ```python Python theme={null} from oxen import DataFrame # Connect to and index the data frame # Note: This must be an existing file committed to the repo # indexing may take a while for large files df = DataFrame("YOUR_USERNAME/YOUR_REPO", "path/to/file.jsonl") # Add a row row_id = data_frame.insert_row({"category": "spam", "message": "Hello, do I have an offer for you!"}) # Get a row by id row = data_frame.get_row_by_id(row_id) print(row) # Update a row row = data_frame.update_row(row_id, {"category": "ham"}) print(row) # Delete a row data_frame.delete_row(row_id) # Get the current changes to the data frame status = data_frame.diff() print(status) # Commit the changes data_frame.commit("Updating data.csv")​ ``` Note: There are currently some limitations to the DataFrame API. 1. You must have **write access** to the repository to use the DataFrame API. This is because it creates a [workspace](/python-api/workspace) on the remote server to index the dataset. 2. Indexing may take a while for large files, and is performed on instantiation of the DataFrame object. 3. The DataFrame API is currently only supported for single files, you cannot yet use it to JOIN datasets across files. ## Datasets as a Vector Database If you have a column in your dataset that contains a vector of floats representing a piece of text or image, you can use Oxen.ai as a vector database to sort by similarity. Embedding a dataset ```python Python theme={null} from oxen import DataFrame # Connect to and index the data frame df = DataFrame("YOUR_USERNAME/YOUR_REPO", "path/to/file.parquet") # Check if the dataset is indexed for embeddings search if not df.is_nearest_neighbors_enabled(): # Enable nearest neighbors search df.enable_nearest_neighbors() # Get an embedding for a specific row (may return multiple embeddings there are multiple results for the query) embed_column = "embedding" embeddings = df.get_embeddings({"prompt": "What is the capital of France?"}, column=embed_column) # Query the data frame embedding = embeddings[0] results = df.query( embedding=embedding, sort_by_similarity_to=embed_column ) for row in results: print(row["prompt"]) ``` If you don't have an embedding column, you can compute one using an Evaluation on the Oxen.ai platform. ## Rendering Images and Links In the dataset viewer, you can render images and links to other files in the dataset. The assumption is that the value in the rows is a relative path to a file in the same repository. For example if we have a directory of images in the `images` directory, we can render the image by using the relative path to the image `images/my_image_0.png`. Rendering images and links In order to enable the rendering, you need to edit the `render` function in the dataset viewer. Go into the edit mode of the dataset, then edit the column you want to render. You can select from a few different rendering options including: `image`, `link`, `markdown`, and `code`. Edit render function This will save metadata to the repository that will be used to render the images and links. To programmatically set the render function, checkout the [file metadata documentation](/concepts/file_metadata). ## Using the UI ### Editing Your Dataset You can edit your dataset directly from the UI by clicking the pencil icon in the upper right of the dataset viewer. This will open the file in an editor that will allow to add, edit, and delete rows and columns. Editing a dataset The editor will not commit any changes to the repository until you use the "Commit" button to write a message and save your changes. Editing a dataset ### Use LLMs to Augment Your Dataset In Oxen.ai, you can generate new columns and rows using LLMs. This is a great way to automatically label your dataset or generate training data for small LLMs from larger models. Click the "Actions" button and select "Run Inference". Oxen.ai Evaluation Simply select a model, write a prompt, and run the model row by row on the dataset. Oxen.ai Evaluation ### Query Your Dataset To query your dataset, write a question in plain English in the search bar. This will automatically translate the question into a SQL query and apply it to the view of your data. For example, you can look at the distribution of question types by asking: ``` What are all the categories sorted by count? ``` Where to find Text2SQL If the query engine makes a mistake, no worries! You can edit the SQL query to get the results you want. # πŸ”₯ Performance Source: https://docs.oxen.ai/examples/data/performance # πŸ–ΌοΈ 1 Million Files Benchmark When we first started working on Oxen.ai, we were inspired by making a tool that would make it easy to collaborate on large datasets that power modern AI research. One dataset that comes to mind is the original [ImageNet](https://image-net.org/) dataset. This dataset spans 1000 object classes and contains > 1,000,000 training images and 100,000 test images. It commonly gets shared as a tarball, zip file, or gets dumped to S3 without much visibility into the data itself. ImageNet A version control system (VCS) would be a much better way to share and iterate on datasets like ImageNet. This is an example of a dataset that hasn't been updated since it's initial release. Backing the dataset with a VCS would allow people to collaborate on the dataset without duplicating data all over the place. In order to do this effectively, the VCS needs to be fast to make the developer experience worth using. Not an easy task, but one we were willing to plow through at Oxen.ai πŸ‚ ## πŸ“Š The Raw Numbers To create this benchmark, we took the 1 million+ images from ImageNet and added them to Oxen, DVC, Git-LFS, and S3. The total time is to get the files from A (local filesystem) to B (remote storage) successfully. The steps to reproduce and the machine specs are in the sections below. Here are the results in ranked order from fastest to slowest. | Tool | Time | Can view data? | | ---------------- | ------------------- | -------------------- | | **πŸ‚ Oxen.ai** | 1 hour and 30 mins | βœ… Yes | | **Tarball + S3** | 2 hours 21 mins | ❌ No | | **aws s3 cp** | 2 hours 48 mins | ❌ No | | **DVC + Local** | 3 hours | ❌ No | | **DVC + S3** | 4 hours and 51 mins | βœ… Yes w/ Other Tools | | **Git-LFS** | 20 hours | ❌ No | Notice that **Oxen is faster than even the laziest of methods**, creating a tarball and uploading it to S3, but with the benefits of being able to view, query, and compare versions of the data. If you would like us to add any other tools to the benchmark, please let us know! ## βš™οΈ Hardware and Network All of the benchmarks were executed on a `t3.2xlarge` EC2 instance with `4 vCPUs` and `16.0 GB of RAM` and a `1TB EBS` volume attached. We found that the size of the EBS volume did impact the IOPs for adding and committing data for all tools. All of the network transfer was within us-west-1 within AWS to S3. ## πŸ‘€ View the Data One of the other advantages of using Oxen.ai, besides raw speed, is that you can view, query and collaborate on the data as soon as you've pushed it to the [web hub](https://oxen.ai). Feel free to explore the end result [here](https://oxen.ai/datasets/ImageNet-1k) in Oxen.ai. ImageNet-Oxen-DataFrame ## 🧐 Why not Git? Everybody knows and loves Git. But we also know that it isn't exactly suited to version data. Trying to add multi-gigabyte datasets can quickly blowup storage costs and cause serious slowdown. And that isn't really Git's purpose, either - GitHub, for instance, doesn't even accept files larger than 100 megabytes. Over the years, however, several attempts have been made to extend Git to gigabyte or even terabyte scale. In 2015 Git-LFS support was added to GitHub, which speeds up pulls by downloading files lazily, replacing tracked files with pointers and retrieving their content upon checkout. Data Version Control (DVC) came out in 2017, employing a similar concept but storing the file contents externally to Git. In theory it sounds great to tie your VCS to the most popular version control system in the world in git. But in practice, it is a bit like trying to fill a swimming pool with a straw. You can do it, but you are tied to the limitations of the git protocols. ## πŸ‚ How does Oxen.ai work? With Oxen.ai, we take a different approach. Rather than trying to extend Git, we built Oxen, taking inspiration from Git where we can. We didn't want to make you learn a completely new tool. If you know how to use git, you know how to use Oxen. But we also designed Oxen specifically to make versioning large amounts of data as fast as possible. Under the hood, Oxen uses Merkle trees, smart network protocols and fast hashing algorithms to reduce the amount of data our repositories store. Unbound by Git, however, we're also able to employ several other optimizations that make Oxen fast such as block-level deduplication, compression, iterating on subtrees, and more. Some of these optimizations are still under development, but we're excited to share what we have so far, and you can find a deeper dive and list of the upcoming features [here](https://oxen-ai.github.io/). All of the code is open source and available on [GitHub](https://github.com/oxen-ai/Oxen). We appreciate any feedback you have and welcome any stars and contributions! *** # πŸƒ Running the Experiments To give you a sense of the process as well as point out the advantages & challenges associated with each method, we ran the following experiments below, listed from slowest to fastest. ## Git + LFS (\~20 hours) Git-LFS is a popular first tool to try since it is already in the Git ecosystem. The problem is that it is painfully slow when it comes to adding, committing, and pushing non-text files. It can also be a bit annoying to remember which files are tracked under LFS vs just regular Git. Many times have I accidentally committed a multi-GB file to git and wondered why my push was taking so long. Removing files from the git merkle tree is a whole other pain. Steps to reproduce: ``` git init git lfs install git lfs track "*.jpg" git add .gitattributes git add images # 61 minutes git commit -m "adding images" # 11 minutes git push origin main # 19 hours ``` Total time: `20+ hours` Adding and committing data locally is not terribly slow (still slower than Oxen). But it does have to hash and copy every file into the hidden `.git` directory. The combination of using a slow hashing algorithm and copying large files makes git-lfs slower than it has to be on `add` and `commit`. The real killer here though is the push πŸ₯±. Pushing data to the remote takes over 20 hours in the case of ImageNet, even on the same network as our other tests. ## DVC + S3 Backend (\~5 hours) DVC is a popular tool, tightly integrated with the Git ecosystem and can be configured for multiple storage backends. You'll see that you have to toggle back and forth between DVC and git with 11 commands to remember and execute. It is easy to make a mistake and track the wrong things in your git repo as well as simply wrap your head around the fact that you are using two different tools to version your data. Steps to reproduce: ``` git init dvc init git status git commit -m "Initialize DVC" dvc add images/ # Executed in 132.12 mins git add images.dvc .gitignore git commit -m "adding images" git remote add origin https://github.com/owner/repository.git dvc remote add --default datastore s3://my-bucket git push origin main dvc push # Executed in 159.55 mins ``` Total Time: `4 hours and 51 mins` As you can see, DVC is not as slow as Git-LFS, but it is significantly more commands to remember and execute. ## DVC + Local Storage Backend (\~3 hours) We wanted to do another test with DVC without any network transfer, purely to test the protocol overhead. Transferring to S3 may not be the best apples to apples comparison, since Oxen also compresses and deduplicates data on the network transfer. ``` git init dvc init git status git commit -m "Initialize DVC" dvc add images/ # Executed in 132.12 mins git add images.dvc .gitignore git commit -m "adding images" dvc remote add -d myremote /home/ubuntu/dvc-remote git push origin main dvc push # Executed in 49.53 mins ``` Total Time: `3 hours` As we'll see below, Oxen is faster than DVC even if you drop the overhead of network transfer. ## Tarball + S3 (\~2 hours 21 mins) I like to call this one, "F' it, let's just create a tarball and upload it to S3". Easy to remember, easy to use, but not very efficient nor effective when it comes to iterating on data. ``` time tar czf imagenet-images.tar.gz images/ # Executed in 114.66 mins time aws s3 cp imagenet-images.tar.gz s3://imagenet-tarball # Executed in 27.13 mins ``` Total Time: `2 hours 21 mins` This may work well for cold storage of data you may rarely want to view again. But for anything else, Oxen is a much better tool. Oxen smartly compresses and creates smaller data chunks behind the scenes while transferring your data across the network, taking advantage of the network bandwidth and reducing the amount of time it takes to upload and download data. ## aws s3 cp (\~2 hours 48 mins) You may be asking yourself, well if the tarball takes so long to create, why not just use the `aws s3 cp` command with the `--recursive` flag? ``` time aws s3 cp --recursive images s3://imagenet-files # Executed in 168.28 mins ``` Total Time: `2 hours 48 mins` This is a bit slower overall than the tarball method, and you still have the same problems of iterating on and viewing the data. By looking at the logs, it looks like the s3 sdk is syncing the files one by one, which accounts for the slowness. ## Oxen.ai (\~1 hour and 30 mins) With Oxen, if you know how to use git, there are no extra commands to remember. With the same commands as plain old git you can initialize, add, commit, and push your data to the remote. Steps to reproduce: ``` oxen init oxen add images # Executed in 41.35 mins oxen commit -m "adding images" # Executed in 50.75 secs oxen config --set-remote origin https://hub.oxen.ai/datasets/ImageNet-1k oxen push # Executed in 49.11 mins ``` Total Time πŸ”₯: `1 hour and 30 mins` If you are curious how Oxen works under the hood, we are working on a detailed technical writeup that dives into the Merkle tree, block-level deduplication, and more [here](https://oxen-ai.github.io/). # Try Oxen.ai for Yourself If you would like to try Oxen.ai for yourself, you can sign up for a free account [here](https://oxen.ai/). All of the code is open source and available on [GitHub](https://github.com/oxen-ai/Oxen). Let us know what you think by joining our [Discord](https://discord.com/invite/s3tBEn7Ptg). # πŸ’Ύ Version Control Source: https://docs.oxen.ai/examples/data/versioning Oxen.ai is built on top of a blazing fast data version control system that allows you to version, branch, and share datasets, model weights, and experiments with your team. Oxen's [open source data version control system](https://github.com/Oxen-AI/Oxen) shines at workflows and data sizes where git or git-lfs fall short. The interface is inspired by git, so that it is easy to learn for engineers, but has a few core differences. Oxen is built from the ground up to handle large datasets with many files or large csvs, parquet files, or other large binary blobs like model weights, videos or 3D assets. The developer tools come with a [CLI](/examples/data/versioning#versioning-101), [HTTP APIs](/http-api), and [Python library](/python-api) to make it easy to integrate into your workflow. ## Versioning 101 On the surface, `oxen` looks a lot like `git`. Users can add, commit, data locally then push to a remote server. Similar to git, by default oxen will create a local copy of the data on your machine in your `.oxen` directory before pushing to the remote server. ```bash CLI theme={null} oxen init oxen add lotsa_data/ oxen commit -m "adding too much data for git" # Create the remote on hub.oxen.ai (or `oxen create-remote --name /`) # and wire it up before pushing: oxen config --set-remote origin https://hub.oxen.ai// oxen push origin main ``` ```python Python theme={null} from oxen import Repo repo = Repo(".") repo.init() repo.add("lotsa_data/") repo.commit("adding too much data for git") repo.set_remote("origin", "https://hub.oxen.ai//") repo.push() ``` The first main difference is that `oxen` comes with a remote `oxen-server` that user's can sync data to. This server also allows you to upload data directly without making local copies. ```bash CLI theme={null} SYNC_DIR=/path/to/data oxen-server start -p 3000 -i 0.0.0.0 ``` Say we had already pushed a large dataset to the remote server, and simply wanted to to add a file to a large dataset like ImageNet with [1 Million Files](/examples/data/performance). You do not want to wait to clone all the files locally just to add yours to the server. ```python Python theme={null} from oxen import RemoteRepo # Connect to the remote client repo = RemoteRepo("my-username/my-repo") # Add the images to the workspace without committing. # Pass `dst=` so the files land under `images/` on the remote. repo.add("images/image_1_000_001.png", dst="images/") repo.add("images/image_1_000_002.png", dst="images/") # Commit the remote changes repo.commit("Adding the 1,000,001st image to the dataset") ``` ```bash CLI theme={null} # Assuming you already are in a local repository with a remote configured # (run `oxen config --set-remote origin ` if you haven't). oxen workspace create --name add-image --branch main # Stage multiple files into the workspace before committing oxen workspace add images/image_1_000_001.png --workspace-name add-image oxen workspace add images/image_1_000_002.png --workspace-name add-image # Commit the remote changes oxen workspace commit \ -m "Adding the 1,000,001st image to the dataset" \ --workspace-name add-image \ --branch main ``` This is just one example of how Oxen.ai enables a more developer friendly workflow for large datasets. There are also optimizations under the hood such as parallel file transfer, scalable merkle trees, and data deduplication to make Oxen go brrr (or mooo?). ## Interfaces The server exposes a REST API that can be used to interact with data. Oxen.ai's clients include a [command line interface](/getting-started/command-line/start_repository), as well as bindings for [Rust](https://github.com/Oxen-AI/Oxen) πŸ¦€, [Python](/python-api) 🐍, and [HTTP interfaces](/http-api) 🌎 to make it easy to integrate into your workflow. ## Installation Oxen makes versioning your datasets as easy as versioning your code. You can install through homebrew or pip or from our [releases page](https://github.com/Oxen-AI/Oxen/releases). ```bash CLI theme={null} brew install oxen ``` ```bash Python theme={null} pip install oxenai ``` ## Remote Workflow Centralized version control systems like Oxen.ai allow you to have remote first workflows where you do not need to have a fully copy of the data on your local machine. Decentralized version control systems like git by default duplicate all the data to every node in your network. Oxen Remote and Local Workflow While the decentralized nature of git makes it easy to maintain full copies of the history across many machines, this is not practical for large datasets. Oxen was designed from the ground up to be able to seamlessly switch between local and remote (centralized) workflows. Only clone what you need, and contribute back to the remote repository when you are done. ### Create a Remote Repository If you do not already have a remote repository, you can create one with a single `README.md` and initial commit so it is immediately cloneable. ```python Python theme={null} from oxen import RemoteRepo # RemoteRepo.create is an instance method β€” construct first, then call create. # The Python client adds a README.md and initial commit by default. repo = RemoteRepo("my-user/my-repo-name") repo.create() ``` ```bash CLI theme={null} # The CLI defaults to an empty repo, so pass --add_readme to include a # README.md and initial commit (the equivalent of the Python default). oxen create-remote --name my-user/my-repo-name --add_readme ``` ```bash cURL theme={null} curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://hub.oxen.ai/api/repos \ -d '{ "namespace": "my-user", "name": "my-repo-name", "description": "A repository for image classification", "is_public": true }' ``` If you want to create an empty repository β€” with no `README.md` and no initial commit β€” pass `empty=True` from Python, or simply omit `--add_readme` from the CLI. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("my-user/my-repo-name") repo.create(empty=True) ``` ```bash CLI theme={null} # The CLI default is an empty repo (no README, no commits). oxen create-remote --name my-user/my-repo-name ``` ```bash cURL theme={null} # The HTTP API creates a bare empty repository by default β€” there is no # `empty` flag because no README is ever added server-side. curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://hub.oxen.ai/api/repos \ -d '{ "namespace": "my-user", "name": "my-repo-name" }' ``` The reason you may want to start with an empty repository is if you already started a local repository and want to push it to the remote repository. This local repository already has a commit history. When pushing to a remote, commit histories must match. Hence we need to start with an empty remote repository without any commits if we want to push a local repository with a commit history. ### Add Files You can add files to the remote repository by passing the path to the file and the destination directory. This will upload the file to the remote repository and stage it for commit. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") repo.add("images/000000002754.jpg", dst="images/") ``` ```bash CLI theme={null} # Stage a file into a workspace before committing oxen workspace add images/000000002754.jpg --workspace-name add-image ``` ```bash cURL theme={null} # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/file/:branch/:dst_dir # Uploads files via multipart form AND commits in one call. curl -X PUT -H "Authorization: Bearer $TOKEN" \ -F "files[]=@images/000000002754.jpg" \ -F "name=Bessie Oxington" \ -F "email=bessie@oxen.ai" \ -F "message=Adding the 1,000,001st image to the dataset" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/file/main/images ``` ### Commit Changes You can commit changes to the remote repository by passing a message. ```python Python theme={null} repo.commit("Adding the 1,000,001st image to the dataset") ``` ```bash CLI theme={null} oxen workspace commit \ -m "Adding the 1,000,001st image to the dataset" \ --workspace-name add-image ``` ```bash cURL theme={null} # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/workspaces/:workspace_id/merge/:branch # Commits the staged files in the workspace and merges them into the branch. curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/merge/main \ -d '{ "author": "Bessie Oxington", "email": "bessie@oxen.ai", "message": "Adding the 1,000,001st image to the dataset" }' ``` ### File Exploration To see the files in the remote repository you can use `ls`. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") print(repo.ls()) ``` ```bash cURL theme={null} # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/dir/:revision/:path # :revision can be a branch name or commit hash. Pass an empty path for the repo root. curl -X GET -H "Authorization: Bearer $TOKEN" \ "https://hub.oxen.ai/api/repos/ox/CatDogBBox/dir/main/" ``` To view a specific directory you can pass the directory name to the `ls` method. Note: the directories are paginated so you will need to use the `page_num` parameter to view the next page of results. There are also `total_pages`, `page_number`, and `total_entries` attributes that give you information about the pagination. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") images_results = repo.ls("images", page_num=1, page_size=10) print(images_results) print(images_results.total_pages) print(images_results.page_number) print(images_results.total_entries) ``` ```bash cURL theme={null} # Pass `page` and `page_size` as query params for pagination. curl -X GET -H "Authorization: Bearer $TOKEN" \ "https://hub.oxen.ai/api/repos/ox/CatDogBBox/dir/main/images?page=1&page_size=10" ``` ### Downloading Data You can download individual files and folders if you do not need the entire data repository for your job. ```bash CLI theme={null} oxen download ox/CatDogBBox annotations/test.csv ``` ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") repo.download("annotations/test.csv") ``` ```bash cURL theme={null} # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/file/:revision/:path # :revision can be a branch name or commit hash curl -X GET -H "Authorization: Bearer $TOKEN" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/file/main/annotations/test.csv \ -o ~/Downloads/test.csv ``` ### Checkout a Branch If you have a data on a separate branch that you want to view you can checkout a branch by passing the branch name to the `checkout` method. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") repo.checkout("my-branch-name") print(repo.ls()) ``` ```bash CLI theme={null} oxen checkout my-branch-name ``` ```bash cURL theme={null} # There is no HTTP "checkout" β€” branches are referenced by name in the URL of # subsequent API calls. To verify a branch exists and read its current commit: # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/branches/:branch_name curl -X GET -H "Authorization: Bearer $TOKEN" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/branches/my-branch-name ``` ### Create a New Branch The `checkout` method also allows you to create a new branch if the branch does not exist. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") repo.checkout("my-new-branch-name", create=True) print(repo.ls()) ``` ```bash CLI theme={null} oxen checkout -b my-new-branch-name ``` ```bash cURL theme={null} # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/branches curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/branches \ -d '{ "from_name": "main", "new_name": "my-new-branch-name" }' ``` ### View Branches To see all the branches in the remote repository you can use the `branches` method. ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("ox/CatDogBBox") print(repo.branches()) ``` ```bash CLI theme={null} # List both local and remote branches from inside a local clone. # To list only remote branches, use: `oxen branch -r origin` oxen branch -a ``` ```bash cURL theme={null} # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/branches curl -X GET -H "Authorization: Bearer $TOKEN" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/branches ``` ### Workspaces Under the hood, the way that we enable remote collaboration is through a concept called a [workspace](/examples/data/workspaces). A workspace can be thought of as an uncommitted working directory that is stored on the server. Just like you can `add` files before committing locally, you can `add` files to a workspace on the remote server before committing. This allows you to build up a set of changes remotely before committing them in bulk. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") # The second positional arg to Workspace is the BRANCH the workspace is tied # to. The optional `workspace_name` gives the workspace a stable identifier # so you can reattach to it later by name. workspace = Workspace(repo, "main", workspace_name="add-images") workspace.add("/path/to/image.png") status = workspace.status() print(status.added_files()) # Commits land on the workspace's branch β€” "main" in this example. workspace.commit("Adding the 1,000,001st image to the dataset") ``` ```bash CLI theme={null} # Run from inside a local clone of the repo. # Workspaces can be addressed by name (-n) or by their server-assigned id (-w). oxen workspace create -n add-images --branch main oxen workspace add image.png -n add-images oxen workspace status -n add-images oxen workspace commit -n add-images -m "Adding the 1,000,001st image to the dataset" --branch main ``` ```bash cURL theme={null} # 1. Get or create a workspace from a base branch curl -X PUT -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/get_or_create \ -d '{ "branch_name": "main", "name": "add-images" }' # 2. Upload and stage a file into the workspace at a destination path # URL Format: /api/repos/:namespace/:repo_name/workspaces/:workspace_id/files/:dst_path curl -X POST -H "Authorization: Bearer $TOKEN" \ -F "file=@/path/to/image.png" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/files/images # 3. Commit the workspace and merge it into the target branch curl -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/merge/main \ -d '{ "author": "Bessie Oxington", "email": "bessie@oxen.ai", "message": "Adding the 1,000,001st image to the dataset" }' ``` The `RemoteRepo.add` method is a shortcut for creating a workspace and adding files to it. It creates a ephemeral workspace and adds the files to it, and deletes the workspace after committing. To learn more about workspaces, check out the [workspaces documentation](/examples/data/workspaces). ### Clone a Remote Repository Remote repositories are identified by a remote URL. This is the URL that you can use to clone the repository. ```python Python theme={null} from oxen import RemoteRepo remote_repo = RemoteRepo("my-user/my-repo-name") remote_repo.create(empty=True) # `url` is a property, not a method β€” no parentheses. print(remote_repo.url) ``` ```bash CLI theme={null} # The remote URL follows the format below. View it with: oxen config --get-remote ``` ```bash cURL theme={null} # Verify the remote repo exists and fetch its metadata. # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name curl -X GET -H "Authorization: Bearer $TOKEN" \ https://hub.oxen.ai/api/repos/my-user/my-repo-name ``` You can use this URL to clone the repository. ```python Python theme={null} # Local Repository from oxen import Repo from oxen import RemoteRepo remote_repo = RemoteRepo("my-user/my-repo-name") remote_repo.create(empty=True) repo_url = remote_repo.url local_repo = Repo("/path/to/local/repo") local_repo.clone(repo_url) ``` Or you can set the remote of an existing local repository to point at the remote repository. ```python Python theme={null} from oxen import Repo from oxen import RemoteRepo remote_repo = RemoteRepo("my-user/my-repo-name") remote_repo.create(empty=True) local_repo = Repo("/path/to/local/repo") local_repo.set_remote("origin", remote_repo.url) ``` ## Local Workflow Local workflow looks a lot like git. The downside is that you have to duplicate all the data locally. The good news is that oxen is much faster than git for large files and repositories. ### Initialize User Each change you make will be associated with a name and email. Set them before you get started so you know who changed what. The user data is saved by default in `~/.config/oxen/user_config.toml`. ```bash CLI theme={null} oxen config --name "Bessie Oxington" --email "bessie@yourcomany.com" ``` ```python Python theme={null} from oxen.user import config_user config_user("Bessie Oxington", "bessie@oxen.ai") ``` ### Create Repository Initialize your first Oxen repository, and commit the first version of your data. ```bash CLI theme={null} # Initialize the repository oxen init # Write data to a file printf '%s\n' 'name,age' 'bob,12' 'jane,13' > people.csv # Stage the data for commit oxen add people.csv # Commit the changes with a message oxen commit -m "Adding my data" ``` ```python Python theme={null} import os from oxen import Repo # Instantiate a Repo object and create the repo directory repo = Repo("/path/to/data", mkdir=True) # Initialize the repository repo.init() # Write data to a file data_path = os.path.join(repo.path, "people.csv") with open(data_path, "w") as f: f.write("name,age\nbob,12\njane,13") # Stage the data for commit repo.add(data_path) # Commit the changes with a message repo.commit("Adding my data") ``` ### Create Branch It is good practice to create a new branch for changes you make to your data. This will allow you to easily compare the parallel versions of your data over time. ```bash CLI theme={null} # Checkout a branch named `modify-data` oxen checkout -b modify-data # Overwrite data in existing file printf '%s\n' 'name,age' 'bob,12' 'jane,13' 'joe,14' > people.csv ``` ```python Python theme={null} import os from oxen import Repo repo = Repo("/path/to/data") # Create a new branch called `modify-data` repo.checkout("modify-data", create=True) # Overwrite data in existing file data_path = os.path.join(repo.path, "people.csv") with open(data_path, "w") as f: f.write("name,age\nbob,12\njane,13\njoe,14") ``` ### Delete Branch Once finished with a branch, you can delete it. ```bash CLI theme={null} # Checkout main branch locally oxen checkout main # Delete 'other_branch' locally oxen branch -d new_branch # may need -D if branch is not merged into main # Delete branch in remote repo oxen push origin --delete new_branch ``` ```python Python theme={null} import os from oxen import Repo # Instantiate a Repo object repo = Repo("/path/to/data") # Checkout the main branch repo.checkout("main") # Delete new_branch. If it has commits not merged into main, oxen will # refuse the delete β€” fully merge first, or use the CLI's -D for a force-delete. repo.branch('new_branch', delete=True) # Delete remote branch repo.push('origin', 'new_branch', delete=True) ``` ```bash cURL theme={null} # Deletes the branch on the remote repository. # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/branches/:branch_name curl -X DELETE -H "Authorization: Bearer $TOKEN" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/branches/new_branch ``` ### Status Check the current state of your local repository by using `oxen status`. Instead of printing out every file that was added/modified/removed (which is unsustainable for large repositories), `oxen` summarizes the changes and lets you page through them. ```bash CLI theme={null} oxen status ``` ```python Python theme={null} from oxen import Repo repo = Repo("/path/to/data") print(repo.status()) ``` ### Restore Changes If you are not happy with the changes you made to your data, you can restore them to the previous commit with the `oxen restore` command. ```bash CLI theme={null} oxen restore --source people.csv ``` ### Commit Changes Once you are happy with the changes you have made to your data, you can commit them to the repository with a new message. ```bash CLI theme={null} oxen add people.csv oxen commit -m "Adding Joe to the dataset" ``` ```python Python theme={null} from oxen import Repo repo = Repo("/path/to/data") # Stage the data for commit data_path = os.path.join(repo.path, "people.csv") repo.add(data_path) # Commit the changes with a message repo.commit("Adding Joe to the dataset") ``` ### View Commit History To see the commit history of your repository, you can use the `oxen log` command. ```bash CLI theme={null} oxen log ``` ```python Python theme={null} from oxen import Repo # Instantiate a Repo object repo = Repo("/path/to/data") # Get the commit history commits = repo.log() ``` ```bash cURL theme={null} # View the commit history of a remote repo at a given revision. # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/commits/history/:revision curl -X GET -H "Authorization: Bearer $TOKEN" \ "https://hub.oxen.ai/api/repos/ox/CatDogBBox/commits/history/main?page=1&page_size=25" ``` ### Checkout Main Branch Once you are done making changes to your data, you can return to the main branch with the `oxen checkout` command. Never fear, the file now has now been reverted to the inital commit again, but your changes will be saved in the branch you created. ```bash CLI theme={null} oxen checkout main ``` ```python Python theme={null} from oxen import Repo # Instantiate a Repo object repo = Repo("/path/to/data") # Checkout the main branch repo.checkout("main") ``` ### List Branches To see the branches in your repository, you can use the `oxen branch` command. ```bash CLI theme={null} oxen branch ``` ```python Python theme={null} from oxen import Repo # Instantiate a Repo object repo = Repo("/path/to/data") # Get the branches print(repo.branches()) ``` ```bash cURL theme={null} # List branches in the remote repository. # URL Format: https://hub.oxen.ai/api/repos/:namespace/:repo_name/branches curl -X GET -H "Authorization: Bearer $TOKEN" \ https://hub.oxen.ai/api/repos/ox/CatDogBBox/branches ``` ### Push Data Once your data has been committed locally, you can sync it to the `oxen-server`. Oxen.ai has a web hub that allows you to collaborate on your data in the cloud. You can create a free account at [https://oxen.ai](https://oxen.ai). ```bash CLI theme={null} # Go create repo at https://oxen.ai # ... oxen config --set-remote origin https://hub.oxen.ai// oxen config --auth hub.oxen.ai oxen push origin main # to push your other branch simply change the branch name from `main` to `modify-data` ``` ```python Python theme={null} # Go create repo at https://oxen.ai # ... # Set where to push the data to (replace and with your remote) repo.set_remote("origin", "https://hub.oxen.ai//") # Set your auth token (defaults to hub.oxen.ai host) oxen.auth.config_auth("YOUR_AUTH_TOKEN") # Push the changes to the remote repo.push() ``` To learn more about setting up authentication and authorization, read our [security documentation here](/getting-started/auth). ### Clone Data Clone your data faster than ever before. Oxen has been optimized to the core to make pulling large datasets as fast as possible. ```bash CLI theme={null} oxen clone https://hub.oxen.ai/ox/CatDogBBox ``` ```python Python theme={null} from oxen import Repo # Construct a Repo at the local destination, then clone into it. repo = Repo("/path/to/dst") repo.clone("https://hub.oxen.ai/ox/CatDogBBox") ``` ### Pull Changes Only pull the changes you need. Oxen will only pull the files that have changed since the last time you pulled. ```bash CLI theme={null} oxen pull origin main ``` ```python Python theme={null} from oxen import Repo repo = Repo("/path/to/repo") repo.pull() ``` # πŸ“¦ Workspaces Source: https://docs.oxen.ai/examples/data/workspaces A workspace is a server-side working tree: stage files, edit data frames, and hold draft state on the remote without committing. A workspace is your repository's working tree, hosted on the server. Just like the uncommitted state of a local repo, you can `add`, `rm`, and modify files in a workspace, and read them back, before anything enters commit history. When you're ready, commit the workspace and the whole set of changes lands as one commit. You never need to download the dataset locally. Because that working tree lives on the server instead of on the client's machine, it can do things a local working directory can't: * **Shared:** Multiple processes, users, or agents can write to the same named workspace and see each other's changes before anyone commits. * **Durable:** Staged data lives on the server and sticks around until you commit it or delete the workspace. If your laptop, app server, or CI job restarts, the staged work is still there. * **Live:** Staged files can be read back through the API right away, and staged tabular files are indexed into DuckDB so you can [query and edit them like a database](#editing-tabular-files-like-a-database). The tradeoff: staged changes are not version history. They have no log of their own, and deleting the workspace discards them. Committing is what makes them permanent. ## Quick start ### Add to an existing repo without cloning it Imagine a repository with 1 million images. Instead of cloning the data, init an empty local repo, point it at the remote, and stage files into a workspace. Don't confuse the workspace name with a branch: `add-images` is just a label for uncommitted remote state on top of `main`. Committing it lands those staged changes on `main` as one commit. ```bash CLI theme={null} # Init only if you have not already setup the repo locally oxen init # Point your local repo to the remote oxen config --set-remote origin https://hub.oxen.ai/ox/ImageNet-1k # Create a named workspace pinned to the latest commit on main oxen workspace create --name add-images --branch main # Stage a single file into the images/ directory of the workspace oxen workspace add /path/to/my_images/image.jpg --directory images/ --workspace-name add-images # See what's staged oxen workspace status --workspace-name add-images # Commit the staged changes to main oxen workspace commit -m "Add new image to images/ directory" -n add-images -b main ``` ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/ImageNet-1k") # Host defaults to 'hub.oxen.ai' # Second arg is the branch the workspace is pinned to. # workspace_name is the stable label for this staging area. workspace = Workspace(repo, "main", workspace_name="add-images") workspace.add("new_images/") status = workspace.status() print(status.added_files()) workspace.commit("Add new images to dataset") # Defaults to the workspace's branch (main) ``` ```bash cURL theme={null} export TOKEN= REPO=https://hub.oxen.ai/api/repos/ox/ImageNet-1k # Use a stable id your client persists. Workspace paths accept either the id or the name. export WORKSPACE_ID=add-images # Create (or fetch) a named workspace on main curl -X PUT "$REPO/workspaces/get_or_create" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"branch_name": "main", "workspace_id": "'"$WORKSPACE_ID"'", "name": "add-images"}' # Stage a single file into the images/ directory of the workspace curl -X POST "$REPO/workspaces/$WORKSPACE_ID/files/images" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@/path/to/my_images/image.jpg" # See what's staged curl "$REPO/workspaces/$WORKSPACE_ID/changes" -H "Authorization: Bearer $TOKEN" # Commit the staged changes to main curl -X POST "$REPO/workspaces/$WORKSPACE_ID/merge/main" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "Add new image to images/ directory", "author": "Bessie", "email": "bessie@oxen.ai"}' ``` ### Bulk-import data into a fresh repo `oxen workspace add` streams files straight to the remote. It never copies them into a local `.oxen` store the way `add β†’ commit β†’ push` does, so you avoid that extra disk and time cost on imports. ```bash CLI theme={null} oxen init oxen config --create-remote --host hub.oxen.ai --scheme https --name ox/ImageNet-1k oxen workspace create # Returns a workspace ID oxen workspace add images/ --workspace-id [WORKSPACE_ID] oxen workspace commit -m "Import 1 million images" -w [WORKSPACE_ID] ``` ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/ImageNet-1k") repo.create() # Create the remote repo first workspace = Workspace(repo, "main") workspace.add("images/") status = workspace.status() print(status.added_files()) workspace.commit("Import 1 million images") ``` ```bash cURL theme={null} export TOKEN= REPO=https://hub.oxen.ai/api/repos/ox/ImageNet-1k # One-shot import: a fresh id is fine because this workspace is unnamed and deleted on commit export WORKSPACE_ID="$(uuidgen)" # Create a workspace to import the data curl -X PUT "$REPO/workspaces/get_or_create" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"branch_name": "main", "workspace_id": "'"$WORKSPACE_ID"'"}' # Stage each file into the images/ directory of the workspace curl -X POST "$REPO/workspaces/$WORKSPACE_ID/files/images" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@images/dog_1.jpg" # Commit the staged changes curl -X POST "$REPO/workspaces/$WORKSPACE_ID/merge/main" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "Import 1 million images", "author": "Bessie", "email": "bessie@oxen.ai"}' ``` The [Driving workspaces over HTTP](#driving-workspaces-over-http) section below walks through the HTTP flow step by step. This is how you build a custom client like a labeling UI, ingestion daemon, or agent without shipping the Oxen CLI or Python SDK. ## How it works Every workspace is pinned to a base commit (usually the tip of a branch at create time). When you add, remove, or modify files, Oxen records a diff against that commit and stores it on the server. Remote Workspace Staged data is durable but unversioned. It lives on the server, survives restarts of both your client and the server, and persists until the workspace is committed or deleted. It does not appear in the repository's commit history until you commit, so there is no log of staged edits, and deleting the workspace discards them without a trace. When you commit a workspace: 1. Oxen applies your staged diff on top of the workspace's base commit to produce a new commit. 2. That new commit is added to a target branch on the remote (see [Committing changes](#committing-changes) for how the target is chosen). 3. If the target branch has advanced past the workspace's base commit, Oxen attempts to merge. Conflicts cause the commit to fail and you'll need to resolve them before retrying. Because workspaces are commit-scoped, two workspaces created from the same branch at different times can see completely different views of the repo. This isolation is intentional, but it also means a long-lived workspace can drift from the branch tip and accumulate conflicts. ## Creating a workspace A workspace is created against a remote repository and a branch. The second argument to the Python `Workspace` constructor is always the **branch**, not the workspace name. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "main", workspace_name="add-images") ``` ```bash CLI theme={null} oxen workspace create -n add-images -b main ``` ```bash cURL theme={null} curl -X PUT "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/get_or_create" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"branch_name": "main", "workspace_id": "add-images", "name": "add-images"}' ``` Python always requires the branch argument. The CLI defaults to your current local branch when you omit `--branch`. Over HTTP, `branch_name` is always required. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "main") # Unnamed workspace on main ``` ```bash CLI theme={null} oxen workspace create # Uses your current local branch; returns a workspace ID ``` The workspace is pinned to whatever commit the branch points at when you create it. For non-empty repositories, that branch must already exist on the remote. ### Named vs. unnamed workspaces Every workspace has an **id**. You can optionally also give it a human-readable **name**. The CLI generates a UUID id on create; over HTTP you supply the id yourself. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "main", workspace_name="add-images") ``` ```bash CLI theme={null} oxen workspace create -n add-images ``` ```bash cURL theme={null} # name is optional, omit it for an unnamed workspace curl -X PUT "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/get_or_create" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"branch_name": "main", "workspace_id": "add-images", "name": "add-images"}' ``` The name matters because of two behavioral differences: | | Unnamed workspace | Named workspace | | --------------------- | ----------------------------------- | --------------------------------------------------------------------------- | | Lifetime after commit | Deleted | Persists, fast-forwarded to the new commit | | Best for | One-shot imports, throwaway staging | Long-lived staging, app draft state, multi-commit or multi-client workflows | Use a **named** workspace when you expect to make multiple commits from the same workspace, when several processes or users will share it, or when an application needs to find its staging area again after a restart (list the workspaces and match on name, or use the [get\_or\_create endpoint](#driving-workspaces-over-http)). Use an **unnamed** workspace for one-off imports where you don't need it to stick around. ### Identifying a workspace in CLI commands Most workspace commands need to know which workspace you're targeting. You can reference a workspace by either its id or its name: * `--workspace-id ` (short `-w`): the auto-generated id returned from `oxen workspace create`. * `--workspace-name ` (short `-n`): the name you set with `--name` at create time. ## Listing workspaces List the workspaces on a remote with `oxen workspace list`. ```bash CLI theme={null} oxen workspace list -r my_remote # Defaults to `origin` if no remote is provided ``` ```bash cURL theme={null} curl "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces" \ -H "Authorization: Bearer $TOKEN" ``` ## Adding files `oxen workspace add` streams a file's contents directly to the server and stages it on the workspace. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "main", workspace_name="add-images") workspace.add("/path/to/image.png") status = workspace.status() print(status.added_files()) ``` ```bash CLI theme={null} oxen workspace add image.png -n add-images oxen workspace status -n add-images ``` ```bash cURL theme={null} # Stage image.png into the images/ directory of the workspace curl -X POST "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/files/images" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@image.png" # See what's staged curl "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/changes" \ -H "Authorization: Bearer $TOKEN" ``` ### Unstaging a file To remove a file you've staged on the workspace (without touching the base repo), unstage it with `oxen workspace rm --staged`. ```bash CLI theme={null} oxen workspace rm --staged image.jpg -n add-images ``` ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "main", workspace_name="add-images") workspace.unstage("image.jpg") # Requires oxen > 0.53.0 ``` ```bash cURL theme={null} curl -X DELETE "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/changes/image.jpg" \ -H "Authorization: Bearer $TOKEN" ``` ### Deleting a file from the base repo `oxen workspace rm` **without** `--staged` stages a deletion of a file that exists in the base repo. When you commit the workspace, that file will be removed from the branch. Use `--staged` if you only want to unstage a previously added file. ```bash CLI theme={null} oxen workspace rm image.jpg -n add-images ``` ```bash cURL theme={null} curl -X DELETE "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/$WORKSPACE_ID/files" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '["image.jpg"]' ``` The Python SDK does not expose staging a deletion yet. Despite the name, `Workspace.rm()` unstages a staged file, the same as `unstage()`. Use the CLI or the HTTP endpoint above to stage a removal from the base repo. ## Editing tabular files like a database Staging isn't limited to whole files. When you open a tabular file (`csv`, `tsv`, `parquet`, `jsonl`, etc.) through the `DataFrame` class, Oxen indexes it into DuckDB inside a workspace. This gives you a queryable, editable database in an uncommitted state. You can insert, update, and delete individual rows and query with SQL, and nothing touches the branch until you commit. `DataFrame("namespace/repo", "path")` creates a workspace under the hood (or reuses one if you pass a `Workspace` / `workspace_name`). You do not need to construct a `Workspace` yourself for the common case. ```python theme={null} from oxen import DataFrame # Connect to and index the data frame in a workspace # Note: this must be an existing file committed to the repo; # indexing may take a while for large files data_frame = DataFrame("datasets/SpamOrHam", "data.tsv") # Add a row (returns a stable row id) row_id = data_frame.insert_row({"category": "spam", "message": "Hello, do I have an offer for you!"}) # Read it back row = data_frame.get_row_by_id(row_id) # Update and delete by row id data_frame.update_row(row_id, {"category": "ham"}) data_frame.delete_row(row_id) # Query the staged state with SQL results = data_frame.query(sql="SELECT category, COUNT(*) FROM df GROUP BY category") # Commit the edits, or call data_frame.restore() to discard them data_frame.commit("Clean up spam labels") ``` This is the machinery behind editing datasets in the Oxen.ai UI and behind [building custom labeling tools](/features/labeling_data). Every cell edit, row insert, and row delete is a staged change in a workspace, batched up until someone commits. It also powers [embeddings search](/features/embeddings), which uses the same DuckDB index to query vector columns without committing. Since workspaces are shared, an agent can insert rows all day while a human reviews the staged changes, and the dataset only gets a new commit when the batch is approved. See the [DataFrame Python API](/python-api/data_frame) for the full interface. ## Committing changes Commit a workspace to land its staged changes as a new commit on the remote. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "main", workspace_name="add-images") # Optional second arg is the target BRANCH to commit onto workspace.commit("adding an image using a workspace", "main") ``` ```bash CLI theme={null} oxen workspace commit -m "adding an image" -n add-images -b main ``` ```bash cURL theme={null} curl -X POST "https://hub.oxen.ai/api/repos/ox/CatDogBBox/workspaces/add-images/merge/main" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "adding an image", "author": "Bessie", "email": "bessie@oxen.ai"}' ``` If you don't provide a target branch, each interface picks a default. Note that the target branch must already exist on the remote, the server will never create one for you: * **Python** commits to the branch the workspace was created from. * **CLI** commits to your current local branch (and errors if you have no current branch). * **HTTP** has no default, the target branch is always part of the URL. ```python Python theme={null} from oxen import RemoteRepo from oxen import Workspace repo = RemoteRepo("ox/CatDogBBox") workspace = Workspace(repo, "my-branch", workspace_name="add-images") workspace.commit("adding an image using a workspace") # Defaults to my-branch ``` ```bash CLI theme={null} oxen workspace commit -m "adding an image" -n add-images # Commits to your current local branch ``` After a successful commit: * An **unnamed** workspace is deleted. * A **named** workspace is fast-forwarded to point at the new commit, so you can keep using it. ### Merge conflicts The target branch advancing past the workspace's base commit is not a problem by itself. As long as the new commits on the branch touched different files, the workspace merges cleanly. A commit only fails with a "workspace is behind" error when a file you staged also changed on the target branch after the workspace was created. There is no rebase command for a workspace. To recover from a conflict: 1. Create a fresh workspace, which will be pinned to the current tip of the branch. 2. Re-stage your changes there. For conflicted files, fetch the branch's current version first and re-apply your edits on top of it. 3. Commit the new workspace, and delete the stale one. ## Driving workspaces over HTTP Everything above is a thin wrapper over the [Repository API](/http-api/index), which means a workspace can be the persistence layer of any application without installing the Oxen CLI or Python SDK. This section walks through the full lifecycle the way an app like a draft editor, labeling backend, or ingestion daemon would use it. All requests are authenticated with your API key: ```bash theme={null} export TOKEN= export REPO=https://hub.oxen.ai/api/repos/ox/CatDogBBox ``` ### 1. Get or create a named workspace `get_or_create` returns an existing workspace when the id already exists, or when a workspace with the given `name` already exists; otherwise it creates one. Persist a stable `workspace_id` in your app (do not mint a fresh UUID on every boot), and pass a `name` so you can also find the workspace by listing. Workspace paths accept either the id or the name, so you can use the same stable string for both in simple apps: ```bash theme={null} export WORKSPACE_ID=draft-editor curl -X PUT "$REPO/workspaces/get_or_create" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"branch_name": "main", "workspace_id": "'"$WORKSPACE_ID"'", "name": "draft-editor"}' ``` To inspect what is already on the remote, list workspaces: ```bash theme={null} curl "$REPO/workspaces" -H "Authorization: Bearer $TOKEN" ``` ### 2. Stage a file `POST` the file as multipart form data to a directory path inside the workspace. Staging on every save is cheap. Each write simply replaces the staged version of the file. ```bash theme={null} curl -X POST "$REPO/workspaces/$WORKSPACE_ID/files/images" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@image.jpg" ``` ### 3. Read staged content back `GET` the same path to read the staged version back before anything is committed. If the file isn't staged in the workspace, the request returns a 404 and you can fall back to the committed version on the branch: ```bash theme={null} # The staged (workspace) version, i.e. the draft curl "$REPO/workspaces/$WORKSPACE_ID/files/images/image.jpg" \ -H "Authorization: Bearer $TOKEN" # The committed version on the branch, used as the fallback curl "$REPO/file/main/images/image.jpg" \ -H "Authorization: Bearer $TOKEN" ``` ### 4. List what's staged The `changes` endpoint returns the workspace's staged additions, modifications, and removals. An app can rebuild its view of what is in draft from this endpoint after a restart. ```bash theme={null} curl "$REPO/workspaces/$WORKSPACE_ID/changes" \ -H "Authorization: Bearer $TOKEN" ``` To unstage a single file without touching the base repo: ```bash theme={null} curl -X DELETE "$REPO/workspaces/$WORKSPACE_ID/changes/images/image.jpg" \ -H "Authorization: Bearer $TOKEN" ``` To unstage several paths at once, `DELETE` the `changes` collection with a JSON body: ```bash theme={null} curl -X DELETE "$REPO/workspaces/$WORKSPACE_ID/changes" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '["images/image.jpg", "data/config.json"]' ``` ### 5. Commit the workspace to a branch When the user hits "publish" (or the batch is approved), merge the workspace into the target branch. All staged changes land as one commit. ```bash theme={null} # Optional: check mergeability first curl "$REPO/workspaces/$WORKSPACE_ID/merge/main" -H "Authorization: Bearer $TOKEN" # Commit curl -X POST "$REPO/workspaces/$WORKSPACE_ID/merge/main" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "Publish edits", "author": "Bessie", "email": "bessie@oxen.ai"}' ``` If a file you staged also changed on the branch, the commit fails with a "workspace is behind" conflict error. See [Merge conflicts](#merge-conflicts) for how to recover. After a successful commit, a named workspace fast-forwards to the new commit and you can start staging again from step 2. The same workspace can serve as the app's staging area indefinitely. ### 6. Clean up Deleting a workspace permanently discards anything still staged in it: ```bash theme={null} curl -X DELETE "$REPO/workspaces/$WORKSPACE_ID" -H "Authorization: Bearer $TOKEN" ``` The full endpoint reference, including batch file upload and the workspace data frame endpoints, lives in the [Repository API docs](/http-api/index). ## Example use cases Workspaces are useful whenever committing on every write would be too noisy, too slow, or premature. The classic cases: editing a repository that's too large to clone, bulk-importing data without paying the disk cost of a local `.oxen` store, batching dozens of changes into one atomic commit, or letting several processes and users build up a staged batch together. If your repo is small enough to clone and the normal `add β†’ commit β†’ push` flow works for you, you don't need a workspace. See the [Version Control guide](/examples/data/versioning) instead. Here are a few things you could build, to get your wheels turning: * **A document editor.** Autosaves stage each edit to a named workspace, and reads fall back from the workspace to the committed branch, so drafts live on the server instead of in a database. Hitting "Publish" commits the workspace to `main` as one commit. * **A data ingestion pipeline.** Workers append raw training examples into a shared workspace all day, then a reviewer fixes bad rows and commits one clean dataset version. This is how [Oxen.ai's labeling tools](/features/labeling_data) work under the hood. * **An AI agent's scratchpad.** An agent writes files and edits data frames in a workspace while a human reviews the staged changes. Good runs become auditable commits, bad runs get deleted without touching history. * **A review queue for community datasets.** Contributors upload images or rows to a shared workspace, like a pull request for data. A maintainer reviews the staged batch and commits it, so `main` only ever contains approved data. * **An edge or sensor data buffer.** Devices push readings into a workspace all day. Committing hourly or daily gives you clean versioned snapshots instead of thousands of tiny commits. * **A model evaluation harness.** Each eval run writes predictions and metrics into a workspace. Commit only the runs worth keeping, then diff commits to compare models over time. * **A moderated media app.** User uploads land in a workspace where moderators can view them through the API. Approval is a commit, rejection is an unstage. # πŸ€– Chat Completions Source: https://docs.oxen.ai/examples/fine-tuning/chat_completions How to fine-tune an LLM on a conversation of messages. This tutorial will show you how to fine-tune an LLM on a history of messages. If you have traces from a chat application or have curated a dataset, you can use them to fine-tune a model to be more accurate, faster, or tailored to your use-case. Often a small, specialized model can outperform a larger, more general model for a specific use-case when it comes to latency, accuracy, and cost. ## Upload Your Dataset For this example, we are teaching the model to answer questions based on context that is supplied in the system prompt. You can follow along with the [Tutorials/CoQA](https://www.oxen.ai/Tutorials/CoQA/file/main/train_coqa.jsonl) dataset containing over 7,000 rows of chat messages. Each row of the dataset contains a conversation between a user and an assistant. dataset This particular dataset focuses on multi-turn conversations. Each example starts by grounding some context taken from a news article or a Wikipedia article. The user then asks a question about the context, and the assistant answers the question. There are multiple back and forth exchanges between the user and the assistant, each question building on the previous one. Here's a few examples from the paper to give you an idea of the task. coqa-questions Notice that you would not be able to answer the second question without the first question being answered first. ## Dataset Format Oxen.ai supports datasets in a variety of file formats, including jsonl, csv, and parquet. The only requirement is that you have a column where each row is a list of messages. Each message is an dictionary with a `role` and `content` key. The `role` can be "system", "user", or "assistant". The `content` is the message content. ```json theme={null} { "conversations": [ { "messages": [ {"role": "system", "content": "You are a helpful assistant that answers questions based on the provided context."}, {"role": "user", "content": "What is the easiest way to fine-tune a model?"}, {"role": "assistant", "content": "Oxen.ai allows you to fine-tune a model with a few clicks. Just upload your dataset, select your base model, and click 'Fine-tune'."}, {"role": "user", "content": "What modalities does Oxen.ai support?"}, {"role": "assistant", "content": "Oxen.ai supports text, image, and video generation."} ] } ] } ``` ## Fine-Tuning The Model Once you have uploaded your dataset, click the "Actions" button and select "Fine-tune a model". Fine-tune button Next select your base model, the messages source, whether you'd like to use LoRA or not. We recommend starting with a smaller model like [Qwen3-0.6B](https://www.oxen.ai/ai/models/qwen-qwen3-0-6b) for faster iteration, or a larger model like [Llama 3.1 8B](https://www.oxen.ai/ai/models/meta-llama-3-1-8b-instruct) for better performance on complex conversations. Fine-tune first page For our Advance Options, you can have control over hyper-parameters and model specifications like learning rate, batch size, and number of epochs. These settings can help you optimize for your specific use case, whether you prioritize training speed, model accuracy, or computational efficiency. Advanced options photo ## Monitoring the Fine-Tune While we're fine-tuning your model, you'll be able to see the configuration, logs, and metrics of the fine-tuning. This helps you track the model's progress and identify if you need to adjust any hyperparameters or stop training early if the model has converged. Metrics example ## Deploying the Model Once your fine-tuning is complete, go to the info page and click "Deploy". Oxen.ai will spin up a dedicated endpoint for your model to access via a chat interface or through the API. After the model is deployed, you can click the "Chat with this model" button to open a chat interface where you can test multi-turn conversations. fine-tuned chatbot This will bring up a chat interface where you can test your model with back-and-forth conversations to see how it maintains context across multiple turns. fine-tuned chatbot ## Model API You can integrate it into your application using the API. The API is OpenAI compatible, so you can use any OpenAI client library to interact with it. The base URL for the API is `https://hub.oxen.ai/api`. For chat completions, you'll send a list of messages that includes the conversation history. Each message should have a `role` (either "user", "assistant", or "system") and `content`. ```bash theme={null} curl -X POST https://hub.oxen.ai/api/ai/chat/completions \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "your-model-id", "messages": [ {"role": "system", "content": "You are a helpful assistant that answers questions based on the provided context."}, {"role": "user", "content": "What is the capital of France?"}, {"role": "assistant", "content": "The capital of France is Paris."}, {"role": "user", "content": "What is its population?"} ] }' ``` Make sure to replace `your-model-id` with the ID of your fine-tuned model. The model will use the entire conversation history to generate contextually appropriate responses. ## Next Steps Feel free to join our [Discord](https://discord.com/invite/s3tBEn7Ptg) and ask us or the community any questions you have, we have a community of developers and machine learning experts who are happy to help you out. # πŸ‘¨β€πŸŽ¨ Image Editing Source: https://docs.oxen.ai/examples/fine-tuning/image_editing How to fine-tune an image editing model in Oxen.ai Oxen.ai allows you to get higher quality image edits with consistent brand assets, characters, products, or your own style with no infrastructure setup required. Fine-tune your models with a few clicks, track results during training, and own all your weights to download and use anywhere. ## The Task For this example, we are going to fine-tune Qwen-Image-Edit to be able to turn a photo of a Yeti Mug from a product catalogue into a photo of the mug being used in the wild. The input images will be the mug we want in the scene on a black background (left) and the output will be the mug in a beautiful scene (right). Reference Image The prompt that generated the image on the right was *"a red headed woman sipping from the mug outdoors"*. Notice we did not have to say "yeti mug", but instead we teach the model that "mug" in our context should always be the mug from the reference image. ## Creating a Repository Oxen.ai repositories are used to store and [version](/examples/data/versioning) your data and models. We can create a new repository by clicking the "Create New Repository" button in your Oxen.ai dashboard. Create Repository You will upload your dataset to the repository, and when the fine-tune is complete, the model weights will be saved to the repository on a branch. By versioning your data and models together, you can always track the data that was used to train the model. ## Uploading the Dataset From your repository, you can click the "Add File" button to upload your dataset. The upload supports unpacking zip files if you want to upload a directory of images. Upload Dataset For image editing models, you will need three things: 1. Input images 2. Reference images 3. Prompts for the edits or changes we want to make Start by collecting the input images, reference images, and uploading them to the repository. You will also need a csv file that will contain the prompts for the edits or changes we want to make. ## Formatting the Dataset When you upload tabular data files (like CSV, JSONL, or Parquet) to Oxen.ai, they get superpowers. For example, you can enable image rendering on your image columns to show as thumbnails in the dataset. To view the images, click the "✏️" edit button above the dataset, then edit the column to enable image rendering. The video below shows the whole process. This lets you view images, reference images, and prompts all in one place. Yeti Images Dataset In this case we have one column called `image` that represents the output we want, and a column called `control_image` that represents the reference image that we want to feed as input. There is a third column for the `prompt` that describes the edits or changes we want to make. The image column needs to contain the **relative path** to the image from the root of the repository. For example, if the image is in the `images` folder, the path should be `images/image_0.png`. ## Kicking off the Fine-Tune The other superpower your csv file gets is that you can kick off a fine-tune from the dataset page. Click the "Actions" button and select "Fine-Tune a Model". Kick off Fine-Tune This will take you to the fine-tune page where you can select the model you want to fine-tune. Select the "Qwen-Image-Edit" model, and make sure the "Control Image" column is set to `control_image` column, the "Image" column is set to `image` column, and the "Prompt" column is set to `prompt` column. Kick off Fine-Tune You can also upload some test images and prompts that will be used as samples during the fine-tune. ## Advanced Parameters Click the "Advanced Parameters" button to see the advanced parameters for the fine-tune. You can set the learning rate, batch size, number of steps, and other parameters here. Fine-Tune Parameters One of the best parts of fine-tuning with Oxen.ai is that we track all your experiments for you, so that you can always refer back to the parameters that worked best for future fine-tunes. ## Monitoring the Fine-Tune While the model is training, you can monitor the progress by clicking the "Samples" tab. This will show you the images that the model has generated so far. Monitor the Fine-Tune ## Deploying the Model When the model has finished training, you can deploy it to a new model by clicking the "Deploy Model" button. Deploy the Model Once the model is deployed, you can use it in the playground or via the API. ```bash theme={null} curl -X POST \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "oxen:ox-busy-scarlet-beaver", "input_image": "https://hub.oxen.ai/api/repos/ox/Oxen-Character-Simple-Vector-Graphic/file/main/images/reference/bloxy_white_bg.png", "prompt": "Add a funny hat to the ox", "num_inference_steps": 20 }' https://hub.oxen.ai/api/ai/images/edit ``` ## Using the Playground Click the "Open Playground" button to use the model in the playground. This allows you to prompt the model with different images and prompts to see how it performs. Before and After The playground will save a history of your prompts and images so that you can refer back to them later. ## Exporting the Model All of the model weights are stored back in your repository when the fine-tune is complete. Navigate to the fine-tune info tab, and you will see a link to the model weights. This is helpful if you want to download the weights to run in ComfyUI or your own infrastructure. Info Tab This will take you to the file viewer where you can download the model safetensors. File Viewer You can also automatically download the weights with the [oxen cli](/getting-started/command-line/start_repository) or [python library](/python-api). ```bash CLI theme={null} oxen download user-name/repo-name path/to/model.safetensors --revision COMMIT_OR_BRANCH ``` ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("user-name/repo-name") repo.download("path/to/model.safetensors", revision="COMMIT_OR_BRANCH") ``` ## Need Help Fine-Tuning? If you need help fine-tuning your model, contact us at [hello@oxen.ai](mailto:hello@oxen.ai) and we are happy to help you get started. # πŸ–ΌοΈ Image Generation Source: https://docs.oxen.ai/examples/fine-tuning/image_generation How to fine-tune an image generation model in Oxen.ai Oxen.ai allows you to fine-tune an image generation model to create higher quality images with consistent brand assets, characters, products, or your own style with no infrastructure setup required. Fine-tune your models with a few clicks, track results during training, and own all your weights to download and use anywhere. ## The Task For this example, we are going to fine-tune [Qwen-Image](https://www.oxen.ai/ai/models/qwen-image) to be able to generate images of models wearing a specific outfit. Reference Image ## Dataset Format When fine-tuning image generation models, you need a dataset that contains the images and descriptions of the images. The expected format is a csv, jsonl, or parquet file with a column that contains the *relative path* to the image in the repository, and a column that contains the description of the image. Dataset Format We'll walk through the process of creating a dataset with the images and captions in the following sections. ## Creating a Repository Oxen.ai repositories are used to store and version your data and models. We can create a new repository by clicking the "Create New Repository" button in your Oxen.ai dashboard. Create Repository You will upload your dataset to the repository, and when the fine-tune is complete, the model weights will be saved to a branch in the repository. By versioning your data and models together, you can always track the data that was used to train the model. ## Uploading the Images From your repository, you can click the "Add File" button to upload your dataset of images. If you upload a zip file, it will automatically be unpacked into the repository in the specified directory. Note: When uploading your images, organize your files in a folder by specifying a "Target Directory" when uploading the files. Upload Dataset ## Turn Your Images into a Dataset In order to fine-tune in Oxen.ai, you need a csv, jsonl, or parquet file that contains the images and their captions. With your images uploaded, you can convert the directory of images into a dataset that can be used for the fine-tune. Navigate to the directory of images and click the "Folder to Dataset" button. Folder to Dataset This will grab all of the relative paths from the folder and create a parquet file with a column called `file_path` that contains the relative path to the image. Folder to Dataset In order to view the images, you will need to enable image rendering on the `file_path` column. Click the "✏️" edit button above the dataset, then edit the column to enable image rendering. The video below shows the whole process. ## Captioning The Images In order for the fine-tune to learn a mapping from text to images, we'll need a prompt column that contains a description of each image. Oxen.ai makes it easy to run models on each row of the dataset in order to automatically generate captions. Click the "Actions" button above the dataset, then select "Run Inference". Run Inference You will need to select a model that is able to go from "image" to "text" from the dropdown on the left. Run Inference Now write a prompt that describes what you want in the caption and any formatting you want to apply. ```text theme={null} Describe the model and the clothing that the model is wearing as if you are writing a prompt for an image generation model. The prompt should be one sentence in length and include the gender as "male" or "female". Also describe their hair color and skin tone (white, tan, black, latina, asian). Only describe the person and outfit. The prompt should start with something like "A male Nike model wearing" or "A female Nike model wearing" {file_path} ``` Note: You must supply the curly braces `{}` around the `file_path` column in the prompt to know what column to use for the image. When you feel good about your prompt after looking at your samples click the "Next ->" button to decide where you want to save the results. By default, the results will create a new version of the existing file. Run Inference Now sit back and relax as the model captions your images 😌 β˜•οΈ. Run Inference ## Kicking off the Fine-Tune With our images captioned, now we can kick off the fine-tune! Click the "Actions" button and select "Fine-Tune a Model". Kick off Fine-Tune This will take you to the fine-tune page where you can select the model you want to fine-tune. Select the "Qwen-Image" model, and make sure the "Image" column is set to the `file_path` column, and the "Prompt" column is set to the `caption` column. Write Prompts To monitor the fine-tune, you can provide a few sample prompts that will be used to generate images during the fine-tune. ## Advanced Parameters Click the "Advanced Parameters" button to see the advanced parameters for the fine-tune. You can set the learning rate, batch size, number of steps, and other parameters here. One of the best parts of fine-tuning with Oxen.ai is that we track all your experiments for you, so that you can always refer back to the parameters that worked best for future fine-tunes. Fine-Tune Parameters Click the "Start Fine-Tune" button and you are off to the races! ## Monitoring the Fine-Tune As the model is training, you can monitor the progress by clicking the "Samples" tab. This will show you the images that the model has generated so far. Monitor the Fine-Tune ## Deploying the Model When the model has finished training, you can deploy it to a new model by clicking the "Deploy Model" button. The deployment will take a few minutes to complete. Deploy the Model Once the model is deployed, you can use it in the playground or via the API. ```bash theme={null} curl -X POST \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "model": "oxen:ox-brave-jade-gecko", "prompt": "A majestic ox standing in a field at sunset", "num_inference_steps": 28 }' https://hub.oxen.ai/api/ai/images/generate ``` ## Using the Playground Click the "Open Playground" button to use the model in the playground. This allows you to prompt the model with different images and prompts to see how it performs. Playground The playground will save a history of your prompts and images so that you can refer back to them later. ## Exporting the Model All of the model weights are stored back in your repository when the fine-tune is complete. Navigate to the fine-tune info tab, and you will see a link to the model weights. This is helpful if you want to download the weights to run in ComfyUI or your own infrastructure. Info Tab This will take you to the file viewer where you can download the model `.safetensors` file. File Viewer You can also automatically download the weights with the [oxen cli](/getting-started/command-line/start_repository) or [python library](/python-api). ```bash CLI theme={null} oxen download user-name/repo-name path/to/model.safetensors --revision COMMIT_OR_BRANCH ``` ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("user-name/repo-name") repo.download("path/to/model.safetensors", revision="COMMIT_OR_BRANCH") ``` ## Need Help Fine-Tuning? If you need help fine-tuning your model, contact us at [hello@oxen.ai](mailto:hello@oxen.ai) and we are happy to help you get started. # πŸ‘οΈ Vision Language Models Source: https://docs.oxen.ai/examples/fine-tuning/image_understanding How to fine-tune a Vision Language Model in Oxen.ai Oxen.ai allows you to fine-tune a Vision Language Model (VLM) to understand images and videos. Fine-tuned VLMs are great way to process data at scale with high throughput, low latency, and high accuracy in your domain. When you can't describe your task in a text prompt, you can fine-tune a VLM to understand it. ## Preparing the dataset When fine-tuning a VLM, you need a dataset that contains the images, user prompts, and responses that are expected from the VLM. The dataset format can be a csv, jsonl, or parquet file with a column that contains the *relative path* to the image in the repository. To see an example of the dataset format, check out the [Tutorials/Geometry3K](https://www.oxen.ai/Tutorials/Geometry3K/file/main/train.parquet) dataset. Each row in this dataset should have an associated image in the repository stored at `images/train/image_{n}.png`. Dataset Format To upload the dataset you can use the [oxen command line interface](/getting-started/command-line/start_repository). Here's an example of creating a repository from the command line and uploading data: ```bash theme={null} # Navigate to the directory containing your dataset cd path/to/data # Set your username and repository name export USERNAME=YOUR_USERNAME export REPO_NAME=YOUR_REPO_NAME # Create a new repository on the remote server oxen create-remote --name $USERNAME/$REPO_NAME # Set the remote origin to the new repository oxen config --set-remote origin https://hub.oxen.ai/$USERNAME/$REPO_NAME # Add the dataset to the repository oxen add . # Push the dataset to the remote server oxen push ``` ## Rendering Images In order to view the images, you will need to enable image rendering on your images column. Click the β€œβœοΈβ€ edit button above the dataset, then edit the column to enable image rendering. The video below shows the whole process. ## Fine-tuning a model With your images labeled and you are happy with the quality and quantity, it is time to kick off your first fine-tune. Click the "Actions" button and select "Fine-Tune a Model". Kick off Fine-Tune This will take you to the fine-tune page where you can select the model you want to fine-tune. Select the `Image to Text` task, and select the `Qwen/Qwen3-VL-2B-Instruct` model. Make sure the "Image" column is set to the proper `image` column, and the "Prompt" and "Response" columns are set to the inputs and outputs you expect. Select Task All you have to do now is click "Start Fine-Tune", sit back, grab a coffee, and watch the model learn. ## Deploying the Model Once the model is trained, you can deploy it to the cloud and start using it in your applications. Click the "Deploy" button and we will spin up a dedicated GPU instance for you. Deploy Model Once the model is deployed, you can chat with it in the UI or via the API. Replace the `model` name with the name of your deployed model. ```bash theme={null} curl -X POST \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "oxen:ox-comfortable-sapphire-locust", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "What is in this image?" }, { "type": "image_url", "image_url": { "url": "https://oxen.ai/assets/images/homepage/hero-ox.png" } } ] } ] }' https://hub.oxen.ai/api/ai/chat/completions ``` For more ways to call the API, check out the [inference](/examples/inference/vision_language_models) examples. ## Downloading the Weights One of the benefits of using Oxen.ai is we give you the flexibility of deploying to our cloud or managing your own infrastructure. If you want to download the model weights, you can click the path to the model weights and download them. ```bash CLI theme={null} oxen download user-name/repo-name path/to/model.safetensors --revision COMMIT_OR_BRANCH ``` ```python Python theme={null} from oxen import RemoteRepo repo = RemoteRepo("user-name/repo-name") repo.download("path/to/model.safetensors", revision="COMMIT_OR_BRANCH") ``` ## Need Help Fine-Tuning? If you need help fine-tuning your model, contact us at [hello@oxen.ai](mailto:hello@oxen.ai) and we are happy to help you get started. # πŸ’¬ Text Generation Source: https://docs.oxen.ai/examples/fine-tuning/text_generation How to fine-tune an LLM for text generation. This tutorial will show you how to fine-tune an LLM for text generation. Text generation is useful for tasks like classification like sentiment analysis where you have a single input and output you want the model to learn. Small language models are great for tasks like this because they are fast and cheap to fine-tune and run. If your application needs to maintain a history of chat messages\[] as context for the model, you should follow the [Chat Completions](/examples/fine-tuning/chat_completions) tutorial. ## Upload Your Dataset For this example, we are teaching the model to classify financial sentiment from text. You can follow along with the [Tutorials/FinancialSentiment](https://www.oxen.ai/Tutorials/FinancialSentiment/file/main/train_financial_sentiment.parquet) dataset containing 2000 rows of text and their corresponding sentiment labels. The dataset has one column for the prompt and one for the sentiment label (positive, negative, or neutral). Oxen supports datasets in a variety of formats, including jsonl, csv, and parquet. datasets-page ## Fine-Tuning The Model Once you have uploaded your dataset, click the "Actions" button and select "Fine-tune a model". Fine-tune button Next select your base model, the prompt source, the response source, whether you'd like to use LoRA or not, and if you want advanced control over the fine-tune. For this example, we are using the [Qwen3-0.6B](https://www.oxen.ai/ai/models/qwen-qwen3-0-6b) model, which is small and fast to fine-tune. Fine-tune first page For our Advance Options, you can have control over hyper-parameters and model specifications like learning rate, batch size, and number of epochs. Advanced options photo ## Monitoring the Fine-Tune While we're fine-tuning your model, you'll be able to see the configuration, logs, and metrics of the fine-tuning. Metrics example ## Deploying the Model Once your fine-tuning is complete, go to the info page and click "Deploy". Oxen.ai will spin up a dedicated endpoint for your model to access via a chat interface or through the API. Deploy example After the model is deployed, you can click the "Chat with this model" button to open a chat interface. fine-tuned chatbot This will bring up a chat interface where you can test your model to see how it performs. fine-tuned chatbot ## Model API You can integrate it into your application using the API. The API is OpenAI compatible, so you can use any OpenAI client library to interact with it. The base URL for the API is `https://hub.oxen.ai/api/ai`. ```bash theme={null} curl -X POST https://hub.oxen.ai/api/ai/chat/completions \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "your-model-id", "messages": [{"role": "user", "content": "The company went bankrupt last week."}] }' ``` Make sure to replace `your-model-id` with the ID of your fine-tuned model. ## Next Steps Feel free to join our [Discord](https://discord.com/invite/s3tBEn7Ptg) and ask us or the community any questions you have, we have a community of developers and machine learning experts who are happy to help you out. # πŸŽ₯ Video Generation Source: https://docs.oxen.ai/examples/fine-tuning/video_generation How to fine-tune a video generation model in Oxen.ai Oxen.ai allows you to fine-tune a video generation model to generate higher quality videos with consistent brand assets, characters, products, or your own style with no infrastructure setup required. Fine-tune your models with a few clicks, deploy your model to an endpoint, and own all your weights to download and use anywhere. You can train on either still images or short video clips. Images are enough to capture a *look*, training on video clips lets the model learn **motion**, and with models that support audio (like LTX-2.3 Pro) the fine-tune can even learn how a character **sounds**.