@almadar/std 16.247.0 → 16.248.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/behaviors/behaviors-registry.json +612 -4
- package/behaviors/lolo/ui/learning/atoms/std-learn-generate.lolo +336 -0
- package/behaviors/lolo-behavior-contracts.json +1 -1
- package/behaviors/registry/factory-signatures.json +1 -1
- package/behaviors/registry/ui/learning/atoms/std-learn-generate.orb +2899 -0
- package/dist/behaviors/exports-reader.js +2481 -2423
- package/dist/behaviors/functions/index.d.ts +125 -1
- package/dist/behaviors/functions/index.js +2478 -2424
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +2482 -2424
- package/dist/behaviors/query.js +2481 -2423
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +9 -1
- package/dist/behaviors-registry.json +612 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2482 -2424
- package/dist/knob-embeddings.hash.json +3 -3
- package/dist/knob-embeddings.json +41 -1
- package/dist/registry/factory-signatures.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
app std-learn-generate [
|
|
2
|
+
version: "1.0.0",
|
|
3
|
+
description: "Generic instrument generator — GENERATE { subject, context } picks one learning instrument for a subject and writes its roster. It lists the installed behaviors under `scope` (optionally narrowed to the `instruments` allow-list), keeps those whose trait listens for ROSTER_LOADED (an instrument that takes its roster at runtime), asks the model to CHOOSE one, then to FILL a roster whose fields are exactly that instrument's ROSTER_LOADED payload; the tool loop checks the roster against those declared fields before it is accepted. GENERATED { behavior, mode, roster } carries the instrument, its mode (the behavior's name, the matchMode a lab router uses) and the roster as JSON, ready to send as that instrument's ROSTER_LOADED. It never writes a program: when no instrument in scope fits or the roster cannot be filled, it emits GENERATE_FAILED { error }. The instrument list is read once and kept for later requests."
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
;; @capabilities "pick a learning instrument for a subject, write an instrument's roster, generate an interactive visualisation for a concept"
|
|
7
|
+
|
|
8
|
+
type CatalogHit = {
|
|
9
|
+
behavior : string!
|
|
10
|
+
description : string
|
|
11
|
+
traits : [trait]!
|
|
12
|
+
vectors : Map string [number]
|
|
13
|
+
}
|
|
14
|
+
type AgentChatMessage = {
|
|
15
|
+
role : "user" | "assistant"
|
|
16
|
+
content : string!
|
|
17
|
+
}
|
|
18
|
+
type AgentToolField = {
|
|
19
|
+
name : string!
|
|
20
|
+
type : string!
|
|
21
|
+
required : boolean
|
|
22
|
+
values : [string]
|
|
23
|
+
properties : [AgentToolField]
|
|
24
|
+
description : string
|
|
25
|
+
}
|
|
26
|
+
type AgentToolInput = {
|
|
27
|
+
event : EventAddress!
|
|
28
|
+
payload : [AgentToolField]
|
|
29
|
+
}
|
|
30
|
+
type LearnInstrument = {
|
|
31
|
+
behavior : string!
|
|
32
|
+
description : string
|
|
33
|
+
fields : [AgentToolField]!
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
uses ToolLoop from "std/behaviors/std-agent-tool-loop"
|
|
37
|
+
|
|
38
|
+
orbital LearnGenerateOrbital {
|
|
39
|
+
|
|
40
|
+
entity LearnGeneration [runtime] {
|
|
41
|
+
id : string!
|
|
42
|
+
subject : string = ""
|
|
43
|
+
context : string = ""
|
|
44
|
+
candidates : [CatalogHit] = []
|
|
45
|
+
instruments : [LearnInstrument] = []
|
|
46
|
+
cursor : number = 0
|
|
47
|
+
chosen : string = ""
|
|
48
|
+
roster : string = ""
|
|
49
|
+
error : string = ""
|
|
50
|
+
status : "idle" | "listing" | "sourcing" | "choosing" | "filling" | "done" | "failed" = idle
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
trait LearnGenerate -> LearnGeneration [lifecycle, instance] {
|
|
54
|
+
config {
|
|
55
|
+
scope : [string] = ["almadar-std/ui/learning"]
|
|
56
|
+
@label "Behavior folders"
|
|
57
|
+
@description "Registry folders (`<prefix>/<topic>[/<tier>]`) the generator may draw instruments from; nothing outside them is ever offered"
|
|
58
|
+
@synonyms "scope, catalog, folders"
|
|
59
|
+
@tier "domain"
|
|
60
|
+
instruments : [string] = []
|
|
61
|
+
@label "Allowed instruments"
|
|
62
|
+
@description "Behavior names (`almadar-std/std-learn-predict`) to choose among; empty offers every instrument in the folders"
|
|
63
|
+
@synonyms "allow list, instrument list"
|
|
64
|
+
@tier "domain"
|
|
65
|
+
chooseEvent : EventAddress = LearnGenerate.CHOOSE
|
|
66
|
+
@label "Choose tool event"
|
|
67
|
+
@description "The address the model's CHOOSE tool fires; a consumer that renames this trait sets it to its own name (`VizGen.CHOOSE`)"
|
|
68
|
+
@tier "internal"
|
|
69
|
+
fillEvent : EventAddress = LearnGenerate.FILL
|
|
70
|
+
@label "Fill tool event"
|
|
71
|
+
@description "The address the model's FILL tool fires; a consumer that renames this trait sets it to its own name (`VizGen.FILL`)"
|
|
72
|
+
@tier "internal"
|
|
73
|
+
statusSlot : slot = main
|
|
74
|
+
@label "Status slot"
|
|
75
|
+
@description "Where the atom shows its one-line status; a page running it in the background sends it to `system`"
|
|
76
|
+
@synonyms "status area, slot"
|
|
77
|
+
@tier "presentation"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
state idle {
|
|
81
|
+
INIT -> idle
|
|
82
|
+
(set @entity.status idle)
|
|
83
|
+
CHOOSE -> idle
|
|
84
|
+
FILL -> idle
|
|
85
|
+
GENERATE -> listing when (= (array/len @entity.instruments) 0)
|
|
86
|
+
(set @entity.subject ?subject)
|
|
87
|
+
(set @entity.context (object/get @payload context ""))
|
|
88
|
+
(set @entity.chosen "")
|
|
89
|
+
(set @entity.roster "")
|
|
90
|
+
(set @entity.error "")
|
|
91
|
+
(set @entity.status listing)
|
|
92
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "Finding an instrument…" })
|
|
93
|
+
(behavior/catalog { paths: @config.scope } {
|
|
94
|
+
emit: { failure: GENERATE_FAILED, success: CATALOG_LISTED }
|
|
95
|
+
})
|
|
96
|
+
GENERATE -> choosing when (> (array/len @entity.instruments) 0)
|
|
97
|
+
(set @entity.subject ?subject)
|
|
98
|
+
(set @entity.context (object/get @payload context ""))
|
|
99
|
+
(set @entity.chosen "")
|
|
100
|
+
(set @entity.roster "")
|
|
101
|
+
(set @entity.error "")
|
|
102
|
+
(set @entity.status choosing)
|
|
103
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "Choosing an instrument…" })
|
|
104
|
+
(emit CONVERSE {
|
|
105
|
+
messages: [{ role: "user", content: (str/concat "Subject: " ?subject (if (= (object/get @payload context "") "") "" (str/concat "\nContext: " (object/get @payload context "")))) }]
|
|
106
|
+
instructions: (str/concat "You pick the one interactive learning instrument that best lets a learner explore the subject. The installed instruments:\n" (str/join (array/map @entity.instruments (fn i (str/concat "- " (object/get @i behavior) ": " (object/get @i description "")))) "\n") "\nCall the CHOOSE tool once with exactly one of them, then reply with one sentence.")
|
|
107
|
+
tools: [{
|
|
108
|
+
event: @config.chooseEvent
|
|
109
|
+
payload: [{ name: "choice", type: "string", required: true, values: (array/map @entity.instruments (fn i (object/get @i behavior))) }]
|
|
110
|
+
}]
|
|
111
|
+
ephemeral: true
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state listing {
|
|
116
|
+
INIT -> listing
|
|
117
|
+
(set @entity.status listing)
|
|
118
|
+
CATALOG_LISTED -> sourcing when (> (array/len (array/filter ?result (fn h (or (= (array/len @config.instruments) 0) (array/includes @config.instruments (object/get @h behavior)))))) 0)
|
|
119
|
+
(let ((hits (array/filter ?result (fn h (or (= (array/len @config.instruments) 0) (array/includes @config.instruments (object/get @h behavior)))))))
|
|
120
|
+
(do
|
|
121
|
+
(set @entity.candidates @hits)
|
|
122
|
+
(set @entity.cursor 0)
|
|
123
|
+
(set @entity.status sourcing)
|
|
124
|
+
(behavior/source { behavior: (object/get (array/first @hits) behavior) } {
|
|
125
|
+
emit: { failure: GENERATE_FAILED, success: SOURCED }
|
|
126
|
+
})))
|
|
127
|
+
CATALOG_LISTED -> failed when (= (array/len (array/filter ?result (fn h (or (= (array/len @config.instruments) 0) (array/includes @config.instruments (object/get @h behavior)))))) 0)
|
|
128
|
+
(set @entity.error "No behavior in scope to choose from")
|
|
129
|
+
(set @entity.status failed)
|
|
130
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
131
|
+
(emit GENERATE_FAILED { error: "No behavior in scope to choose from" })
|
|
132
|
+
GENERATE_FAILED -> failed
|
|
133
|
+
(set @entity.error ?error)
|
|
134
|
+
(set @entity.status failed)
|
|
135
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
136
|
+
(emit GENERATION_FAILED { error: ?error })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
state sourcing {
|
|
140
|
+
INIT -> sourcing
|
|
141
|
+
(set @entity.status sourcing)
|
|
142
|
+
SOURCED -> sourcing when (< (+ @entity.cursor 1) (array/len @entity.candidates))
|
|
143
|
+
(let ((fields (array/flatten (array/map (object/get ?result orbitals []) (fn o (array/flatten (array/map (object/get @o traits []) (fn t (array/flatten (array/map (array/filter (object/get (object/get @t stateMachine {}) events []) (fn e (= (object/get @e key "") "ROSTER_LOADED"))) (fn e (array/filter (object/get @e payloadSchema []) (fn f (!= (object/get @f name "") "mode"))))))))))))))
|
|
144
|
+
(do
|
|
145
|
+
(set @entity.instruments (if (> (array/len @fields) 0) (array/append @entity.instruments { behavior: (object/get (array/nth @entity.candidates @entity.cursor) behavior), description: (object/get (array/nth @entity.candidates @entity.cursor) description ""), fields: @fields }) @entity.instruments))
|
|
146
|
+
(set @entity.cursor (+ @entity.cursor 1))
|
|
147
|
+
(behavior/source { behavior: (object/get (array/nth @entity.candidates (+ @entity.cursor 1)) behavior) } {
|
|
148
|
+
emit: { failure: GENERATE_FAILED, success: SOURCED }
|
|
149
|
+
})))
|
|
150
|
+
SOURCED -> choosing when (>= (+ @entity.cursor 1) (array/len @entity.candidates))
|
|
151
|
+
(let ((fields (array/flatten (array/map (object/get ?result orbitals []) (fn o (array/flatten (array/map (object/get @o traits []) (fn t (array/flatten (array/map (array/filter (object/get (object/get @t stateMachine {}) events []) (fn e (= (object/get @e key "") "ROSTER_LOADED"))) (fn e (array/filter (object/get @e payloadSchema []) (fn f (!= (object/get @f name "") "mode"))))))))))))))
|
|
152
|
+
(do
|
|
153
|
+
(set @entity.instruments (if (> (array/len @fields) 0) (array/append @entity.instruments { behavior: (object/get (array/nth @entity.candidates @entity.cursor) behavior), description: (object/get (array/nth @entity.candidates @entity.cursor) description ""), fields: @fields }) @entity.instruments))
|
|
154
|
+
(set @entity.status choosing)
|
|
155
|
+
(emit INSTRUMENTS_READY {})))
|
|
156
|
+
GENERATE_FAILED -> failed
|
|
157
|
+
(set @entity.error ?error)
|
|
158
|
+
(set @entity.status failed)
|
|
159
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
160
|
+
(emit GENERATION_FAILED { error: ?error })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
state choosing {
|
|
164
|
+
INIT -> choosing
|
|
165
|
+
(set @entity.status choosing)
|
|
166
|
+
INSTRUMENTS_READY -> choosing when (> (array/len @entity.instruments) 0)
|
|
167
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "Choosing an instrument…" })
|
|
168
|
+
(emit CONVERSE {
|
|
169
|
+
messages: [{ role: "user", content: (str/concat "Subject: " @entity.subject (if (= @entity.context "") "" (str/concat "\nContext: " @entity.context))) }]
|
|
170
|
+
instructions: (str/concat "You pick the one interactive learning instrument that best lets a learner explore the subject. The installed instruments:\n" (str/join (array/map @entity.instruments (fn i (str/concat "- " (object/get @i behavior) ": " (object/get @i description "")))) "\n") "\nCall the CHOOSE tool once with exactly one of them, then reply with one sentence.")
|
|
171
|
+
tools: [{
|
|
172
|
+
event: @config.chooseEvent
|
|
173
|
+
payload: [{ name: "choice", type: "string", required: true, values: (array/map @entity.instruments (fn i (object/get @i behavior))) }]
|
|
174
|
+
}]
|
|
175
|
+
ephemeral: true
|
|
176
|
+
})
|
|
177
|
+
INSTRUMENTS_READY -> failed when (= (array/len @entity.instruments) 0)
|
|
178
|
+
(set @entity.error "No instrument in scope takes a roster")
|
|
179
|
+
(set @entity.status failed)
|
|
180
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
181
|
+
(emit GENERATION_FAILED { error: "No instrument in scope takes a roster" })
|
|
182
|
+
CHOOSE -> choosing
|
|
183
|
+
(set @entity.chosen ?choice)
|
|
184
|
+
ASSISTANT_REPLIED -> filling when (array/some @entity.instruments (fn i (= (object/get @i behavior) @entity.chosen)))
|
|
185
|
+
(set @entity.status filling)
|
|
186
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "Writing the roster…" })
|
|
187
|
+
(emit CONVERSE {
|
|
188
|
+
messages: [{ role: "user", content: (str/concat "Subject: " @entity.subject (if (= @entity.context "") "" (str/concat "\nContext: " @entity.context))) }]
|
|
189
|
+
instructions: (str/concat "You write the content roster for the learning instrument " @entity.chosen ": " (object/get (array/find @entity.instruments (fn i (= (object/get @i behavior) @entity.chosen))) description "") "\nCall the FILL tool once with a roster of realistic, accurate, pedagogically useful content about the subject. If the tool reports issues, call it again with them fixed. Then reply with one sentence.")
|
|
190
|
+
tools: [{
|
|
191
|
+
event: @config.fillEvent
|
|
192
|
+
payload: [{ name: "roster", type: "object", required: true, properties: (object/get (array/find @entity.instruments (fn i (= (object/get @i behavior) @entity.chosen))) fields []) }]
|
|
193
|
+
}]
|
|
194
|
+
ephemeral: true
|
|
195
|
+
})
|
|
196
|
+
ASSISTANT_REPLIED -> failed when (not (array/some @entity.instruments (fn i (= (object/get @i behavior) @entity.chosen))))
|
|
197
|
+
(set @entity.error "The model chose no installed instrument")
|
|
198
|
+
(set @entity.status failed)
|
|
199
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
200
|
+
(emit GENERATION_FAILED { error: "The model chose no installed instrument" })
|
|
201
|
+
TOOLS_FAILED -> failed
|
|
202
|
+
(set @entity.error ?error)
|
|
203
|
+
(set @entity.status failed)
|
|
204
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
205
|
+
(emit GENERATION_FAILED { error: ?error })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
state filling {
|
|
209
|
+
INIT -> filling
|
|
210
|
+
(set @entity.status filling)
|
|
211
|
+
FILL -> filling
|
|
212
|
+
(set @entity.roster (json/stringify ?roster))
|
|
213
|
+
ASSISTANT_REPLIED -> done when (!= @entity.roster "")
|
|
214
|
+
(set @entity.status done)
|
|
215
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "Instrument ready" })
|
|
216
|
+
(emit GENERATED { behavior: @entity.chosen, mode: (array/last (str/split @entity.chosen "/")), roster: @entity.roster })
|
|
217
|
+
ASSISTANT_REPLIED -> failed when (= @entity.roster "")
|
|
218
|
+
(set @entity.error "The model wrote no roster that matches the instrument")
|
|
219
|
+
(set @entity.status failed)
|
|
220
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
221
|
+
(emit GENERATION_FAILED { error: "The model wrote no roster that matches the instrument" })
|
|
222
|
+
TOOLS_FAILED -> failed
|
|
223
|
+
(set @entity.error ?error)
|
|
224
|
+
(set @entity.status failed)
|
|
225
|
+
(render-ui @config.statusSlot { type: typography, variant: caption, role: status, content: "No instrument could be generated" })
|
|
226
|
+
(emit GENERATION_FAILED { error: ?error })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
state done {
|
|
230
|
+
INIT -> done
|
|
231
|
+
(set @entity.status done)
|
|
232
|
+
GENERATE -> listing when (= (array/len @entity.instruments) 0)
|
|
233
|
+
(set @entity.subject ?subject)
|
|
234
|
+
(set @entity.context (object/get @payload context ""))
|
|
235
|
+
(set @entity.chosen "")
|
|
236
|
+
(set @entity.roster "")
|
|
237
|
+
(set @entity.status listing)
|
|
238
|
+
(behavior/catalog { paths: @config.scope } {
|
|
239
|
+
emit: { failure: GENERATE_FAILED, success: CATALOG_LISTED }
|
|
240
|
+
})
|
|
241
|
+
GENERATE -> choosing when (> (array/len @entity.instruments) 0)
|
|
242
|
+
(set @entity.subject ?subject)
|
|
243
|
+
(set @entity.context (object/get @payload context ""))
|
|
244
|
+
(set @entity.chosen "")
|
|
245
|
+
(set @entity.roster "")
|
|
246
|
+
(set @entity.status choosing)
|
|
247
|
+
(emit INSTRUMENTS_READY {})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
state failed {
|
|
251
|
+
INIT -> failed
|
|
252
|
+
(set @entity.status failed)
|
|
253
|
+
GENERATE -> listing when (= (array/len @entity.instruments) 0)
|
|
254
|
+
(set @entity.subject ?subject)
|
|
255
|
+
(set @entity.context (object/get @payload context ""))
|
|
256
|
+
(set @entity.chosen "")
|
|
257
|
+
(set @entity.roster "")
|
|
258
|
+
(set @entity.error "")
|
|
259
|
+
(set @entity.status listing)
|
|
260
|
+
(behavior/catalog { paths: @config.scope } {
|
|
261
|
+
emit: { failure: GENERATE_FAILED, success: CATALOG_LISTED }
|
|
262
|
+
})
|
|
263
|
+
GENERATE -> choosing when (> (array/len @entity.instruments) 0)
|
|
264
|
+
(set @entity.subject ?subject)
|
|
265
|
+
(set @entity.context (object/get @payload context ""))
|
|
266
|
+
(set @entity.chosen "")
|
|
267
|
+
(set @entity.roster "")
|
|
268
|
+
(set @entity.error "")
|
|
269
|
+
(set @entity.status choosing)
|
|
270
|
+
(emit INSTRUMENTS_READY {})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
emits {
|
|
274
|
+
GENERATED -> external {
|
|
275
|
+
behavior : string!
|
|
276
|
+
mode : string!
|
|
277
|
+
roster : string!
|
|
278
|
+
}
|
|
279
|
+
@description "The chosen instrument, its mode (the behavior's name) and its roster as JSON, ready to send as that instrument's ROSTER_LOADED"
|
|
280
|
+
GENERATION_FAILED -> external {
|
|
281
|
+
error : string!
|
|
282
|
+
}
|
|
283
|
+
@description "No instrument could be generated for the subject; nothing is shown"
|
|
284
|
+
CONVERSE {
|
|
285
|
+
messages : [AgentChatMessage]!
|
|
286
|
+
instructions : string
|
|
287
|
+
tools : [AgentToolInput]
|
|
288
|
+
ephemeral : boolean
|
|
289
|
+
}
|
|
290
|
+
CATALOG_LISTED {
|
|
291
|
+
result : [CatalogHit]!
|
|
292
|
+
}
|
|
293
|
+
SOURCED {
|
|
294
|
+
result : SExpr!
|
|
295
|
+
}
|
|
296
|
+
INSTRUMENTS_READY {
|
|
297
|
+
}
|
|
298
|
+
GENERATE_FAILED {
|
|
299
|
+
error : string!
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
listens {
|
|
304
|
+
GENERATE -> external {
|
|
305
|
+
subject : string!
|
|
306
|
+
context : string
|
|
307
|
+
}
|
|
308
|
+
@description "Generate an instrument and roster for this subject; `context` is any extra the model should know (the lesson's wording, what to visualise)"
|
|
309
|
+
@synonyms "visualise, make an instrument, generate"
|
|
310
|
+
CHOOSE -> external {
|
|
311
|
+
choice : string!
|
|
312
|
+
}
|
|
313
|
+
FILL -> external {
|
|
314
|
+
roster : SExpr!
|
|
315
|
+
}
|
|
316
|
+
LearnGenerateTools.ASSISTANT_REPLIED -> ASSISTANT_REPLIED
|
|
317
|
+
LearnGenerateTools.ASSISTANT_REPLY_FAILED -> TOOLS_FAILED
|
|
318
|
+
TOOLS_FAILED {
|
|
319
|
+
error : string!
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
trait LearnGenerateTools = ToolLoop.traits.AgentToolLoop {
|
|
325
|
+
config {
|
|
326
|
+
quiet: true
|
|
327
|
+
maxSteps: 4
|
|
328
|
+
cancelKey: "std-learn-generate"
|
|
329
|
+
}
|
|
330
|
+
listens {
|
|
331
|
+
LearnGenerate.CONVERSE -> CONVERSE
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
page "/learn-generate" as LearnGeneratePage -> LearnGenerate, LearnGenerateTools
|
|
336
|
+
}
|