This project has been created as part of the 42 curriculum by smikhail.
Call Me Maybe is a function-calling system that translates natural-language requests into structured, machine-executable function calls using a small Large Language Model (Qwen/Qwen3-0.6B, ~600 million parameters).
Goal: given a user prompt like "What is the sum of 2 and 3?", the system produces:
{
"prompt": "What is the sum of 2 and 3?",
"name": "fn_add_numbers",
"parameters": {"a": 2.0, "b": 3.0}
}instead of answering "The answer is 5" — it identifies the right function and the right arguments.
Key innovation: the system uses constrained decoding to guarantee 100% valid, parseable JSON output. Small LLMs left unconstrained produce valid JSON only ~30% of the time; with constrained decoding this reaches near-100%.
- Python 3.10+
uvpackage manager
# From the project root directory:
uv syncThis installs all dependencies (numpy, pydantic, torch, transformers, huggingface-hub, and the local llm_sdk).
# Default (reads from data/input/, writes to data/output/)
uv run python -m src
# Custom paths
uv run python -m src \
--functions_definition data/input/functions_definition.json \
--input data/input/function_calling_tests.json \
--output data/output/function_calling_results.jsonmake install # install dependencies
make run # run with default paths
make debug # run under pdb debugger
make clean # remove __pycache__, .mypy_cache, etc.
make lint # run flake8 + mypyLanguage models generate text one token at a time. At each step they output logits — raw scores over every word-piece in the vocabulary (~150k tokens for Qwen). Normally you pick the highest-scoring token. Constrained decoding modifies this by setting invalid tokens to −∞ before selection, so they can never be chosen.
The JSON we produce always has a fixed shape:
{"name": "<fn_name>", "parameters": {"<p1>": <v1>, ...}}
We exploit this structure with a simple 4-stage pipeline:
| Stage | What happens |
|---|---|
| Inject scaffold | Fixed strings ("name": ", ", "parameters": {, etc.) are encoded and appended directly using model.encode() — no token search needed |
| fn_name | Constrained decode: only tokens whose text is a prefix of a known function name are allowed. Stops at exact match |
| param_string | Allow all tokens except those containing ". Stop when the model's top score goes to a " token |
| param_number | Allow only digit/./- tokens. Stop when , or } outscores any digit token |
BPE tokenizers don't always have single-character tokens. {, :, ", etc. may only appear as part of multi-character tokens. Rather than searching the vocabulary for each character, we let model.encode() handle it correctly.
-
State machine over grammar parsers: a grammar-based approach (like Earley or GLR parsers over a JSON grammar) is more general but much more complex. Since our JSON shape is fixed, a linear state machine is simpler, faster, and easier to understand.
-
Greedy token selection (argmax): we always pick the highest-logit valid token. Temperature sampling would introduce randomness and reduce reliability. For a deterministic, schema-compliant output, greedy is optimal.
-
Scaffold injection via
model.encode(): rather than spending tokens searching for"or{in the vocabulary (which may not be single tokens in Qwen's BPE), we encode fixed fragments directly and append them. This is fast and always correct. -
Pydantic for all data validation: all models use
pydantic.BaseModel. This catches schema mismatches early (bad JSON types, missing keys) and provides clear error messages. -
Graceful error handling everywhere: the
FunctionCaller.call()method catches all exceptions and returns a fallback result rather than crashing. The program never exits unexpectedly.
- JSON validity: 100% — the constrained decoder mathematically cannot produce invalid JSON because it only selects tokens that maintain structural validity at every step.
- Function selection accuracy: >90% — the LLM is given the full function descriptions in the prompt, so it learns context about which function matches the request. Constrained decoding then locks in the choice.
- Speed: Each prompt requires ~50–150 LLM forward passes on Qwen3-0.6B. On Apple Silicon (MPS) this takes ~1–3 seconds per prompt; on CPU ~5–15 seconds. All 11 test prompts finish well within 5 minutes.
-
BPE tokenizer complexity: Qwen uses Byte-Pair Encoding where a single character like
"may not be its own token. Solved by injecting fixed fragments withmodel.encode()instead of searching the vocabulary character-by-character. -
Function name tokenization: function names like
fn_substitute_string_with_regexare long and likely split across many tokens. The_fn_name_valid_tokensmethod handles this by allowing any token whose text, appended to the accumulated name, remains a valid prefix of some function name. -
Number termination: deciding when a number is "complete" (e.g.,
2,265,3.14) is tricky. We compare the logit of the best digit token vs. the logit of separator tokens (,,}) and stop when the separator wins. -
String termination: similar issue. We stop when the model's preference for a closing
"exceeds its preference for any non-quote token.
-
Manual testing: run
uv run python -m srcand inspectdata/output/function_calling_results.jsonfor correct JSON structure and plausible values. -
JSON validation: parse the output file with
json.load()— 100% of entries must parse successfully. -
Schema validation: every entry must have exactly
prompt,name,parameterskeys;namemust be one of the defined function names; parameter types must match the schema. -
Edge cases tested:
- Simple arithmetic (2 numbers)
- Large numbers (265 + 345)
- String arguments (greet, reverse)
- Multi-parameter string functions (regex substitution with 3 params)
- Square root (single number parameter)
# Ensure data/input/ contains both JSON files, then:
uv run python -m src
# Expected output (data/output/function_calling_results.json):
[
{
"prompt": "What is the sum of 2 and 3?",
"name": "fn_add_numbers",
"parameters": {"a": 2.0, "b": 3.0}
},
{
"prompt": "Greet shrek",
"name": "fn_greet",
"parameters": {"name": "shrek"}
}
]- Qwen3 Model Card (Hugging Face)
- Constrained Decoding Survey (Willard & Louf, 2023)
- JSON Schema Specification
- Pydantic Documentation
- uv — Python package manager
AI assistance was used to:
- Review and identify bugs in the initial code (mismatched APIs between files, missing classes)
- Suggest the scaffold-injection approach for fixed JSON structure
- Generate docstrings and README sections