Skip to content

Commit 4d387de

Browse files
authored
feat: JSON Schema grammar enhancements (#388)
* feat(JSON Schema grammar): `prefixItems`, `minItems`, `maxItems` support * feat(JSON Schema grammar): improve inferred types * feat(JSON Schema grammar): object `additionalProperties`, `minProperties`, `maxProperties` * feat(JSON Schema grammar): string `minLength`, `maxLength`, `format` * feat(function calling): params `description` support * feat(function calling): document JSON Schema type properties on Functionary chat function types * docs: how to reduce hallucinations when using JSON schema grammar * fix: bugs * chore: update dependencies
1 parent bc6cfe3 commit 4d387de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+5122
-1394
lines changed

.vitepress/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {createRequire} from "node:module";
33
import process from "process";
44
import {fileURLToPath} from "url";
55
import fs from "fs-extra";
6-
import {createContentLoader, defineConfig, HeadConfig} from "vitepress";
6+
import {createContentLoader, defineConfig, HeadConfig, Plugin as VitepressPlugin} from "vitepress";
77
import {transformerTwoslash} from "@shikijs/vitepress-twoslash";
88
import ts from "typescript";
99
import envVar from "env-var";
@@ -308,7 +308,7 @@ export default defineConfig({
308308
GitChangelog({
309309
repoURL: () => "https://github.com/withcatai/node-llama-cpp",
310310
cwd: path.join(__dirname, "..", "docs")
311-
}),
311+
}) as VitepressPlugin,
312312
GitChangelogMarkdownSection({
313313
exclude: (id) => (
314314
id.includes(path.sep + "api" + path.sep) ||
@@ -318,7 +318,7 @@ export default defineConfig({
318318
sections: {
319319
disableContributors: true
320320
}
321-
}),
321+
}) as VitepressPlugin,
322322
BlogPageInfoPlugin({
323323
include: (id) => id.includes(path.sep + "blog" + path.sep) && !id.endsWith(path.sep + "blog" + path.sep + "index.md")
324324
})

docs/guide/grammar.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
---
2+
outline: deep
3+
---
14
# Using Grammar
25
Use this to enforce a model to generate response in a specific format of text, like `JSON` for example.
36

@@ -69,11 +72,11 @@ console.log(JSON.parse(a2));
6972
The [`llama.createGrammarForJsonSchema(...)`](../api/classes/Llama.md#creategrammarforjsonschema) creates a [`LlamaJsonSchemaGrammar`](../api/classes/LlamaJsonSchemaGrammar)
7073
from a GBNF grammar generated a based on the [JSON schema](https://json-schema.org/learn/getting-started-step-by-step) you provide.
7174

72-
It only supports [a small subset of the JSON schema spec](../api/type-aliases/GbnfJsonSchema.md),
75+
It only supports [a subset of the JSON schema spec](../api/type-aliases/GbnfJsonSchema.md),
7376
but it's enough to generate useful JSON objects using a text generation model.
7477

75-
Many features of [JSON schema spec](https://json-schema.org/learn/getting-started-step-by-step) are not supported here on purpose,
76-
as those features don't align well with the way models generate text and are prone to [hallucinations](https://en.wikipedia.org/wiki/Hallucination_(artificial_intelligence)).
78+
Some features of [JSON schema spec](https://json-schema.org/learn/getting-started-step-by-step) are not supported on purpose,
79+
as those features don't align well with the way models generate text, and are too prone to [hallucinations](https://en.wikipedia.org/wiki/Hallucination_(artificial_intelligence)).
7780
Workarounds for the missing features that you can implement with the supported set of features often lead to improved generation quality.
7881

7982
To see what subset of the JSON schema spec is supported, see the [`GbnfJsonSchema` type](../api/type-aliases/GbnfJsonSchema.md) and follow its sub-types.
@@ -134,6 +137,41 @@ console.log(
134137
);
135138
```
136139

140+
### Reducing Hallucinations When Using JSON Schema Grammar {#reducing-json-schema-hallucinations}
141+
When forcing a model to follow a specific JSON schema in its response, the model isn't aware of the entire schema being enforced on it.
142+
To avoid hallucinations, you need to inform the model in some way what are your expectations from its response.
143+
144+
To do that, you can:
145+
* Explain to the model what you expect in the prompt itself.
146+
<br />
147+
You can do that by giving a brief explanation of what you expect,
148+
or by dumping the entire JSON schema in the prompt (which can eat up a lot of tokens, thus is not recommended).
149+
* Force the model to output self-explanatory keys as part of its response, so it can then generate values for those keys.
150+
* Use a combination of both.
151+
152+
The technique used in [the above example](#json-schema) forces the model to output the given keys, and then lets the model generate the values for those keys:
153+
1. The model is forced to generate the text `{"positiveWordsInUserMessage": [`, and then we let it finish the syntax of the JSON array with only strings.
154+
2. When it finishes the array, we force it to <br />generate the text <span>`, "userMessagePositivityScoreFromOneToTen": `</span>, and then we let it generate a number.
155+
3. Finally, we force it to generate the text `, "nameOfUser": `, and then we let it generate either a string or `null`.
156+
157+
This technique allows us to get the desired result without explaining to the model what we want in advance.
158+
While this method works great in this example, it may not work as well in other cases that need some explanation.
159+
160+
For example, let's say we force the model to generate an array with at least 2 items and at most 5 items;
161+
if we don't provide any prior explanation for this requirement (either by using a self-explanatory key name or in the prompt),
162+
then the model won't be able to "plan" the entire content of the array in advance,
163+
which can lead it to generate inconsistent and unevenly spread items.
164+
It can also make the model repeat the existing value in different forms or make up wrong values,
165+
just so it can follow the enforced schema.
166+
167+
The key takeaway is that to reduce hallucinations and achieve great results when using a JSON schema grammar,
168+
you need to ensure you inform the model of your expectations in some way.
169+
170+
::: tip NOTE
171+
When using [function calling](./function-calling.md), the model is always aware of the entire schema being enforced on it,
172+
so there's no need to explain the schema in the prompt.
173+
:::
174+
137175
## Creating Your Own Grammar {#custom-grammar}
138176
To create your own grammar, read the [GBNF guide](https://github.com/ggerganov/llama.cpp/blob/f5fe98d11bdf9e7797bcfb05c0c3601ffc4b9d26/grammars/README.md) to create a GBNF grammar file.
139177

eslint.config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default tseslint.config({
9090
after: true
9191
}],
9292
"@stylistic/comma-style": ["error", "last"],
93-
"@stylistic/comma-dangle": ["error", "never"],
93+
"@stylistic/comma-dangle": ["warn", "never"],
9494
"no-var": ["error"],
9595
"import/order": ["error", {
9696
groups: ["builtin", "external", "internal", "parent", "sibling", "index", "type", "object", "unknown"],
@@ -142,7 +142,8 @@ export default tseslint.config({
142142
{blankLine: "always", prev: "*", next: "method"}
143143
]
144144
}],
145-
"@stylistic/no-trailing-spaces": ["warn"]
145+
"@stylistic/no-trailing-spaces": ["warn"],
146+
"@stylistic/no-multi-spaces": ["warn"]
146147
}
147148
}, {
148149
files: ["**/**.{,c,m}ts"],

llama/addon/AddonGrammar.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,54 @@ AddonGrammar::~AddonGrammar() {
3434
}
3535
}
3636

37+
Napi::Value AddonGrammar::isTextCompatible(const Napi::CallbackInfo& info) {
38+
const std::string testText = info[0].As<Napi::String>().Utf8Value();
39+
40+
auto parsed_grammar = llama_grammar_init_impl(nullptr, grammarCode.c_str(), rootRuleName.c_str());
41+
42+
// will be empty if there are parse errors
43+
if (parsed_grammar == nullptr) {
44+
Napi::Error::New(info.Env(), "Failed to parse grammar").ThrowAsJavaScriptException();
45+
return Napi::Boolean::New(info.Env(), false);
46+
}
47+
48+
const auto cpts = unicode_cpts_from_utf8(testText);
49+
const llama_grammar_rules & rules = llama_grammar_get_rules(parsed_grammar);
50+
llama_grammar_stacks & stacks_cur = llama_grammar_get_stacks(parsed_grammar);
51+
52+
for (const auto & cpt : cpts) {
53+
const llama_grammar_stacks stacks_prev = llama_grammar_get_stacks(parsed_grammar);
54+
55+
llama_grammar_accept(rules, stacks_prev, cpt, stacks_cur);
56+
57+
if (stacks_cur.empty()) {
58+
// no stacks means that the grammar failed to match at this point
59+
llama_grammar_free_impl(parsed_grammar);
60+
return Napi::Boolean::New(info.Env(), false);
61+
}
62+
}
63+
64+
for (const auto & stack : stacks_cur) {
65+
if (stack.empty()) {
66+
// an empty stack means that the grammar has been completed
67+
llama_grammar_free_impl(parsed_grammar);
68+
return Napi::Boolean::New(info.Env(), true);
69+
}
70+
}
71+
72+
llama_grammar_free_impl(parsed_grammar);
73+
return Napi::Boolean::New(info.Env(), false);
74+
}
75+
3776
void AddonGrammar::init(Napi::Object exports) {
38-
exports.Set("AddonGrammar", DefineClass(exports.Env(), "AddonGrammar", {}));
77+
exports.Set(
78+
"AddonGrammar",
79+
DefineClass(
80+
exports.Env(),
81+
"AddonGrammar",
82+
{
83+
InstanceMethod("isTextCompatible", &AddonGrammar::isTextCompatible),
84+
}
85+
)
86+
);
3987
}

llama/addon/AddonGrammar.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "llama.h"
33
#include "common/common.h"
44
#include "llama-grammar.h"
5+
#include "unicode.h"
56
#include "napi.h"
67
#include "addonGlobals.h"
78

@@ -15,5 +16,7 @@ class AddonGrammar : public Napi::ObjectWrap<AddonGrammar> {
1516
AddonGrammar(const Napi::CallbackInfo& info);
1617
~AddonGrammar();
1718

19+
Napi::Value isTextCompatible(const Napi::CallbackInfo& info);
20+
1821
static void init(Napi::Object exports);
1922
};

0 commit comments

Comments
 (0)