neetnestor commited on
Commit
5254a31
1 Parent(s): 19eaf6a

Add custom grammar support

Browse files
Files changed (4) hide show
  1. dist/index.js +0 -0
  2. index.html +25 -0
  3. src/index.js +171 -134
  4. style/style.css +4 -0
dist/index.js CHANGED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -33,6 +33,31 @@
33
  ></label>
34
  </form>
35
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  <div class="card">
37
  <form>
38
  <label class="container"
 
33
  ></label>
34
  </form>
35
  </div>
36
+ <div class="card">
37
+ <form>
38
+ <label for="grammar-selection" class="container"
39
+ ><span><b>Grammar</b></span>
40
+ <select id="grammar-selection" value="json">
41
+ <option value="json">JSON (Default)</option>
42
+ <option value="custom">Custom EBNF Grammar</option>
43
+ </select></label
44
+ >
45
+ </form>
46
+ </div>
47
+ <div id="ebnf-grammar-container" class="card hidden">
48
+ <form>
49
+ <label for="ebnf-grammar" class="container"
50
+ ><span><b>Custom EBNF Grammar</b></span>
51
+ <textarea
52
+ id="ebnf-grammar"
53
+ dir="ltr"
54
+ placeholder="Type your custom EBNF grammar..."
55
+ rows="1"
56
+ ></textarea
57
+ >
58
+ </label>
59
+ </form>
60
+ </div>
61
  <div class="card">
62
  <form>
63
  <label class="container"
src/index.js CHANGED
@@ -2,151 +2,188 @@ import { prebuiltAppConfig, CreateMLCEngine } from "@charliefruan/web-llm";
2
  import hljs from "highlight.js";
3
  import ace from "ace-builds";
4
 
5
- // required for ace to resolve module correctly
6
  require("ace-builds/src-noconflict/mode-javascript");
7
  require("ace-builds/webpack-resolver");
8
 
9
  // DO NOT REMOVE
10
  // Required for user input type definition to be eval
11
- const { Type } = require('@sinclair/typebox');
12
 
13
  let engine = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- const availableModels = prebuiltAppConfig.model_list
16
- .filter(
17
- (m) =>
18
- m.model_id.startsWith("Llama-3-8B") ||
19
- m.model_id.startsWith("Llama-3-70B") ||
20
- m.model_id.startsWith("Llama-3.1-8B") ||
21
- m.model_id.startsWith("Llama-3.1-70B") ||
22
- m.model_id.startsWith("Llama-3.2-3B") ||
23
- m.model_id.startsWith("Hermes-2") ||
24
- m.model_id.startsWith("Hermes-3") ||
25
- m.model_id.startsWith("Phi-3")
26
- )
27
- .map((m) => m.model_id);
28
- let selectedModel = availableModels[0];
29
-
30
- availableModels.forEach((modelId) => {
31
- const option = document.createElement("option");
32
- option.value = modelId;
33
- option.textContent = modelId;
34
- document.getElementById("model-selection").appendChild(option);
35
- });
36
- document.getElementById("model-selection").value = selectedModel;
37
- document.getElementById("model-selection").onchange = (e) => {
38
- selectedModel = e.target.value;
39
- engine = null;
40
- };
41
- document.getElementById(
42
- "prompt"
43
- ).value = `Hermione Granger is a character in Harry Potter. Please fill in the following information about this character in JSON format.
44
- Name is a string of character name.
45
- House is one of Gryffindor, Hufflepuff, Ravenclaw, Slytherin.
46
- Blood status is one of Pure-blood, Half-blood, Muggle-born.
47
- Occupation is one of Student, Professor, Ministry of Magic, Other.
48
- Wand is an object with wood, core, and length.
49
- Alive is a boolean.
50
- Patronus is a string.
51
- `;
52
-
53
- // JSON editor setup
54
- const editor = ace.edit("schema", {
55
- // mode: "ace/mode/javascript",
56
- mode: "ace/mode/javascript",
57
- theme: "ace/theme/github",
58
- wrap: true,
59
- });
60
- editor.setTheme("ace/theme/github");
61
- editor.setValue(`Type.Object({
62
- "name": Type.String(),
63
- "house": Type.Enum({
64
- "Gryffindor": "Gryffindor",
65
- "Hufflepuff": "Hufflepuff",
66
- "Ravenclaw": "Ravenclaw",
67
- "Slytherin": "Slytherin",
68
- }),
69
- "blood_status": Type.Enum({
70
- "Pure-blood": "Pure-blood",
71
- "Half-blood": "Half-blood",
72
- "Muggle-born": "Muggle-born",
73
- }),
74
- "occupation": Type.Enum({
75
- "Student": "Student",
76
- "Professor": "Professor",
77
- "Ministry of Magic": "Ministry of Magic",
78
- "Other": "Other",
79
- }),
80
- "wand": Type.Object({
81
- "wood": Type.String(),
82
- "core": Type.String(),
83
- "length": Type.Number(),
84
- }),
85
- "alive": Type.Boolean(),
86
- "patronus": Type.String(),
87
- })`);
88
-
89
- // Generate button
90
- document.getElementById("generate").onclick = async () => {
91
- const schemaInput = editor.getValue();
92
- let T;
93
- try {
94
- T = eval(schemaInput);
95
- } catch (e) {
96
- console.error("Invalid schema", e);
97
- return;
98
- }
99
- const schema = JSON.stringify(T);
100
-
101
- if (!engine) {
102
- engine = await CreateMLCEngine(selectedModel, {
103
- initProgressCallback: (progress) => {
104
- console.log(progress);
105
- document.getElementById("output").textContent = progress.text;
106
- },
107
- });
108
- }
109
- const request = {
110
- stream: true,
111
- stream_options: { include_usage: true },
112
- messages: [
113
- {
114
- role: "user",
115
- content: document.getElementById("prompt").value,
116
- },
117
- ],
118
- max_tokens: 128,
119
- response_format: {
120
- type: "json_object",
121
- schema: schema,
122
- },
123
  };
124
 
125
- let curMessage = "";
126
- let usage = null;
127
- const generator = await engine.chatCompletion(request);
128
- for await (const chunk of generator) {
129
- const curDelta = chunk.choices[0]?.delta.content;
130
- if (curDelta) {
131
- curMessage += curDelta;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  }
133
- if (chunk.usage) {
134
- usage = chunk.usage;
 
 
 
 
 
 
 
135
  }
136
- document.getElementById("output").textContent = curMessage;
137
- }
138
- const finalMessage = await engine.getMessage();
139
- console.log(finalMessage);
140
- if (hljs) {
141
- document.getElementById("output").innerHTML = hljs.highlight(finalMessage, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  language: "json",
143
  }).value;
144
- } else {
145
- document.getElementById("output").textContent = finalMessage;
146
- }
147
- if (usage) {
148
- const stats = usage['extra'];
149
- document.getElementById("stats").textContent = `Prefill: ${stats['prefill_tokens_per_s'].toFixed(2)}, Decode: ${stats['decode_tokens_per_s'].toFixed(2)}, Grammar Init: ${stats['grammar_init_ms'].toFixed(2)}, Grammar Overhead Per Token: ${stats['grammar_per_token_ms'].toFixed(2)}`;
150
- document.getElementById("stats").classList.remove("hidden");
151
- }
152
- };
 
 
 
 
 
 
 
2
  import hljs from "highlight.js";
3
  import ace from "ace-builds";
4
 
5
+ // Required for ace to resolve the module correctly
6
  require("ace-builds/src-noconflict/mode-javascript");
7
  require("ace-builds/webpack-resolver");
8
 
9
  // DO NOT REMOVE
10
  // Required for user input type definition to be eval
11
+ const { Type } = require("@sinclair/typebox");
12
 
13
  let engine = null;
14
+ let useCustomGrammar = false;
15
+ let customGrammar = String.raw`main ::= basic_array | basic_object
16
+ basic_any ::= basic_number | basic_string | basic_boolean | basic_null | basic_array | basic_object
17
+ basic_integer ::= ("0" | "-"? [1-9] [0-9]*) ".0"?
18
+ basic_number ::= ("0" | "-"? [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)?
19
+ basic_string ::= (([\"] basic_string_1 [\"]))
20
+ basic_string_1 ::= "" | [^"\\\x00-\x1F] basic_string_1 | "\\" escape basic_string_1
21
+ escape ::= ["\\/bfnrt] | "u" [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9] [A-Fa-f0-9]
22
+ basic_boolean ::= "true" | "false"
23
+ basic_null ::= "null"
24
+ basic_array ::= "[" ("" | ws basic_any (ws "," ws basic_any)*) ws "]"
25
+ basic_object ::= "{" ("" | ws basic_string ws ":" ws basic_any ( ws "," ws basic_string ws ":" ws basic_any)*) ws "}"
26
+ ws ::= [\n\t]*`;
27
 
28
+ document.addEventListener("DOMContentLoaded", () => {
29
+ // Ensure elements are loaded before using them
30
+ const grammarSelection = document.getElementById("grammar-selection");
31
+ const ebnfContainer = document.getElementById("ebnf-grammar-container");
32
+ const modelSelection = document.getElementById("model-selection");
33
+ const ebnfTextarea = document.getElementById("ebnf-grammar");
34
+ const promptTextarea = document.getElementById("prompt");
35
+ const outputDiv = document.getElementById("output");
36
+ const statsParagraph = document.getElementById("stats");
37
+
38
+ // Initialize the custom grammar textarea
39
+ ebnfTextarea.value = customGrammar;
40
+
41
+ // Update grammar value on change
42
+ ebnfTextarea.onchange = (ev) => {
43
+ customGrammar = ev.target.value;
44
+ console.log("Grammar updated: ", customGrammar);
45
+ };
46
+
47
+ // Handle grammar selection changes
48
+ grammarSelection.onchange = (ev) => {
49
+ console.log("Grammar selection changed:", ev.target.value);
50
+ if (ev.target.value === "json") {
51
+ ebnfContainer.classList.add("hidden");
52
+ useCustomGrammar = false;
53
+ } else {
54
+ ebnfContainer.classList.remove("hidden");
55
+ useCustomGrammar = true;
56
+ }
57
+ };
58
+
59
+ // Populate model selection dropdown
60
+ const availableModels = prebuiltAppConfig.model_list
61
+ .filter(
62
+ (m) =>
63
+ m.model_id.startsWith("Llama-3-8B") ||
64
+ m.model_id.startsWith("Llama-3-70B") ||
65
+ m.model_id.startsWith("Llama-3.1-8B") ||
66
+ m.model_id.startsWith("Llama-3.1-70B") ||
67
+ m.model_id.startsWith("Llama-3.2-3B") ||
68
+ m.model_id.startsWith("Hermes-2") ||
69
+ m.model_id.startsWith("Hermes-3") ||
70
+ m.model_id.startsWith("Phi-3")
71
+ )
72
+ .map((m) => m.model_id);
73
+
74
+ let selectedModel = availableModels[0];
75
+
76
+ availableModels.forEach((modelId) => {
77
+ const option = document.createElement("option");
78
+ option.value = modelId;
79
+ option.textContent = modelId;
80
+ modelSelection.appendChild(option);
81
+ });
82
+
83
+ modelSelection.value = selectedModel;
84
+
85
+ modelSelection.onchange = (e) => {
86
+ selectedModel = e.target.value;
87
+ engine = null; // Reset the engine when the model changes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  };
89
 
90
+ // JSON editor setup with Ace
91
+ const editor = ace.edit("schema", {
92
+ mode: "ace/mode/javascript",
93
+ theme: "ace/theme/github",
94
+ wrap: true,
95
+ });
96
+
97
+ editor.setTheme("ace/theme/github");
98
+ editor.setValue(`Type.Object({
99
+ "name": Type.String(),
100
+ "house": Type.Enum({
101
+ "Gryffindor": "Gryffindor",
102
+ "Hufflepuff": "Hufflepuff",
103
+ "Ravenclaw": "Ravenclaw",
104
+ "Slytherin": "Slytherin",
105
+ }),
106
+ "blood_status": Type.Enum({
107
+ "Pure-blood": "Pure-blood",
108
+ "Half-blood": "Half-blood",
109
+ "Muggle-born": "Muggle-born",
110
+ }),
111
+ "occupation": Type.Enum({
112
+ "Student": "Student",
113
+ "Professor": "Professor",
114
+ "Ministry of Magic": "Ministry of Magic",
115
+ "Other": "Other",
116
+ }),
117
+ "wand": Type.Object({
118
+ "wood": Type.String(),
119
+ "core": Type.String(),
120
+ "length": Type.Number(),
121
+ }),
122
+ "alive": Type.Boolean(),
123
+ "patronus": Type.String(),
124
+ })`);
125
+
126
+ // Set initial prompt
127
+ promptTextarea.value = `Hermione Granger is a character in Harry Potter. Please fill in the following information about this character in JSON format.`;
128
+
129
+ // Generate button click handler
130
+ document.getElementById("generate").onclick = async () => {
131
+ const schemaInput = editor.getValue();
132
+ let T;
133
+ try {
134
+ T = eval(schemaInput);
135
+ } catch (e) {
136
+ console.error("Invalid schema", e);
137
+ return;
138
  }
139
+ const schema = JSON.stringify(T);
140
+
141
+ if (!engine) {
142
+ engine = await CreateMLCEngine(selectedModel, {
143
+ initProgressCallback: (progress) => {
144
+ console.log(progress);
145
+ outputDiv.textContent = progress.text;
146
+ },
147
+ });
148
  }
149
+
150
+ const request = {
151
+ stream: true,
152
+ stream_options: { include_usage: true },
153
+ messages: [{ role: "user", content: promptTextarea.value }],
154
+ max_tokens: 128,
155
+ response_format: useCustomGrammar
156
+ ? { type: "grammar", grammar: customGrammar }
157
+ : { type: "json_object", schema: schema },
158
+ };
159
+
160
+ let curMessage = "";
161
+ let usage = null;
162
+ const generator = await engine.chatCompletion(request);
163
+
164
+ for await (const chunk of generator) {
165
+ const curDelta = chunk.choices[0]?.delta.content;
166
+ if (curDelta) curMessage += curDelta;
167
+ if (chunk.usage) usage = chunk.usage;
168
+ outputDiv.textContent = curMessage;
169
+ }
170
+
171
+ const finalMessage = await engine.getMessage();
172
+ outputDiv.innerHTML = hljs.highlight(finalMessage, {
173
  language: "json",
174
  }).value;
175
+
176
+ if (usage) {
177
+ statsParagraph.textContent = `Prefill: ${usage.extra.prefill_tokens_per_s.toFixed(
178
+ 2
179
+ )}, Decode: ${usage.extra.decode_tokens_per_s.toFixed(
180
+ 2
181
+ )}, Grammar Init Overhead: ${usage.extra.grammar_init_ms.toFixed(
182
+ 2
183
+ )}, Grammar Per Token Overhead: ${usage.extra.grammar_per_token_ms.toFixed(
184
+ 2
185
+ )}`;
186
+ statsParagraph.classList.remove("hidden");
187
+ }
188
+ };
189
+ });
style/style.css CHANGED
@@ -180,6 +180,10 @@ button:hover {
180
  cursor: pointer;
181
  }
182
 
 
 
 
 
183
  @media (min-width: 1600px) {
184
  body {
185
  padding: 4rem;
 
180
  cursor: pointer;
181
  }
182
 
183
+ #ebnf-grammar {
184
+ height: 24rem;
185
+ }
186
+
187
  @media (min-width: 1600px) {
188
  body {
189
  padding: 4rem;