Dependency parsing extracts grammatical relationships between words in a sentence. In this tutorial we will build a lightweight tool that sends text to an LLM and returns structured dependency trees in JSON. It is useful for quick linguistic analysis, preprocessing pipelines, or prototyping without training a custom spaCy model.
What you'll need
Before starting, make sure you have the following:
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK installed:
pip install openai
Step 1: Configure the Oxlo.ai client
I keep my API key in an environment variable so it does not leak into notebooks or git history. The client setup is a single line because Oxlo.ai is fully OpenAI-compatible.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key=os.environ.get("OXLO_API_KEY", "YOUR_OXLO_API_KEY")
)
Step 2: Design the system prompt
The prompt is the entire product here. I instruct the model to act as a Universal Dependencies parser and emit only a JSON array of token objects. I include a one-shot example so the model does not hallucinate labels outside the UD set.
SYSTEM_PROMPT = """You are a dependency parser. Given a sentence, return a JSON object with a key "tokens" containing a list of every token in the sentence, in order.
Each token must be an object with these exact keys:
- id: 1-based index
- word: the surface form
- head: the id of the syntactic head, or 0 for the root
- deprel: a Universal Dependencies v2 relation such as nsubj, obj, det, case, root
Rules:
- Do not omit punctuation.
- Use head=0 and deprel="root" for exactly one token.
- Return only valid JSON, no markdown fences.
Example input: "She enjoys pasta."
Example output:
{
"tokens": [
{"id": 1, "word": "She", "head": 2, "deprel": "nsubj"},
{"id": 2, "word": "enjoys", "head": 0, "deprel": "root"},
{"id": 3, "word": "pasta", "head": 2, "deprel": "obj"},
{"id": 4, "word": ".", "head": 2, "deprel": "punct"}
]
}"""
Step 3: Build the parsing function
I use JSON mode so the model is constrained to valid JSON, then wrap the call in a small function that extracts the list and handles the occasional empty response. I default to Llama 3.3 70B because it follows structural instructions well, but you can swap in qwen-3-32b or kimi-k2.6 if you are parsing multilingual text.
import json
from typing import List, Dict
def parse_sentence(text: str, model: str = "llama-3.3-70b") -> List[Dict]:
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Parse this sentence: {text}"},
],
response_format={"type": "json_object"},
temperature=0.1,
)
raw = response.choices[0].message.content
parsed = json.loads(raw)
return parsed.get("tokens", [])
Step 4: Validate and display the tree
Raw JSON is hard to read, so I add a small printer that checks for a single root and prints a simple table. This also acts as a sanity check on the model output.
def print_dependencies(tokens: List[Dict]):
root_count = sum(1 for t in tokens if t.get("deprel") == "root")
if root_count != 1:
raise ValueError(f"Expected exactly one root, found {root_count}")
print(f"{'ID':>3} {'Token':<12} {'Head':>4} {'Relation'}")
print("-" * 32)
for t in tokens:
print(f"{t['id']:>3} {t['word']:<12} {t['head']:>4} {t['deprel']}")
def to_conllu(tokens: List[Dict]) -> str:
lines = []
for t in tokens:
lines.append(
f"{t['id']}\t{t['word']}\t_\t_\t_\t_\t{t['head']}\t{t['deprel']}\t_\t_"
)
return "\n".join(lines)
Step 5: Run a small test suite
I feed the parser a few sentences that cover different structures: a simple transitive clause, a prepositional phrase, and a question. This lets me verify that head indices and relations look correct without writing unit tests.
if __name__ == "__main__":
sentences = [
"The cat sat on the mat.",
"What did she build with Oxlo.ai?",
"A fast, cheap parser saves hours of annotation.",
]
for sent in sentences:
print(f"\nSentence: {sent}")
try:
tokens = parse_sentence(sent)
print_dependencies(tokens)
except Exception as e:
print(f"Error: {e}")
Run it
Save the script as dep_parser.py, export your key, and run it. You should see a table for each sentence. Here is the output I got for the first sentence:
$ export OXLO_API_KEY="sk-..."
$ python dep_parser.py
Sentence: The cat sat on the mat.
ID Token Head Relation
--------------------------------
1 The 2 det
2 cat 3 nsubj
3 sat 0 root
4 on 6 case
5 the 6 det
6 mat 3 obl
7 . 3 punct
Wrap-up
This tool is already useful for quick annotation, but you can push it further. Two concrete next steps: add a lemma field to the prompt and export the results to CoNLL-U so you can import them into annotation software, or cache responses in a local SQLite database to avoid repeating identical requests against your Oxlo.ai quota.
Because Oxlo.ai uses flat per-request pricing, parsing a batch of long sentences costs the same as short ones. If you are moving from a token-based provider, that difference adds up quickly. See https://oxlo.ai/pricing for details.
Top comments (0)