DEV Community

shashank ms
shashank ms

Posted on

Building a Dependency Parsing Tool with LLM

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)