@jpetit/toolkit 3.0.23 → 3.1.2
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/assets/prompts/creators/create-solution.tpl.txt +10 -0
- package/assets/prompts/creators/create-statement.tpl.txt +21 -0
- package/assets/prompts/creators/create-translation.tpl.txt +5 -0
- package/assets/prompts/creators/private-test-cases.txt +6 -0
- package/assets/prompts/creators/sample-test-cases.txt +6 -0
- package/assets/prompts/creators/system-prompt.txt +2 -0
- package/assets/prompts/examples/statement-coda.tex +7 -0
- package/assets/prompts/examples/statement.tex +19 -0
- package/assets/prompts/generators/efficiency.md +41 -0
- package/assets/prompts/generators/hard.md +47 -0
- package/assets/prompts/generators/random.md +39 -0
- package/assets/prompts/proglangs/cc.md +3 -0
- package/assets/prompts/proglangs/py.md +40 -0
- package/lib/ai.ts +60 -4
- package/lib/cleaner.ts +24 -13
- package/lib/compilers/base.ts +70 -14
- package/lib/compilers/clojure.ts +21 -10
- package/lib/compilers/gcc.ts +4 -33
- package/lib/compilers/ghc.ts +4 -40
- package/lib/compilers/gxx.ts +4 -33
- package/lib/compilers/index.ts +9 -0
- package/lib/compilers/java.ts +105 -0
- package/lib/compilers/python3.ts +44 -37
- package/lib/compilers/run-clojure.ts +101 -0
- package/lib/compilers/run-haskell.ts +26 -22
- package/lib/compilers/run-python.ts +29 -35
- package/lib/compilers/rust.ts +39 -0
- package/lib/create-with-jutgeai.ts +407 -0
- package/lib/create-with-template.ts +55 -0
- package/lib/data.ts +6 -0
- package/lib/doctor.ts +86 -6
- package/lib/generate.ts +132 -290
- package/lib/helpers.ts +48 -0
- package/lib/inspector.ts +253 -0
- package/lib/jutge_api_client.ts +4631 -0
- package/lib/maker.ts +202 -289
- package/lib/settings.ts +26 -17
- package/lib/tui.ts +25 -15
- package/lib/types.ts +40 -5
- package/lib/upload.ts +216 -0
- package/lib/utils.ts +82 -14
- package/lib/versions.ts +46 -0
- package/package.json +50 -11
- package/toolkit/about.ts +43 -0
- package/toolkit/ai.ts +44 -18
- package/toolkit/check.ts +16 -0
- package/toolkit/clean.ts +16 -26
- package/toolkit/compilers.ts +4 -4
- package/toolkit/config.ts +91 -0
- package/toolkit/create.ts +30 -58
- package/toolkit/doctor.ts +15 -11
- package/toolkit/generate.ts +195 -98
- package/toolkit/index.ts +32 -21
- package/toolkit/make.ts +12 -48
- package/toolkit/upgrade.ts +9 -0
- package/toolkit/upload.ts +19 -0
- package/toolkit/create-jutge-ai.ts +0 -101
- package/toolkit/create-template.ts +0 -55
- package/toolkit/create-wizard.ts +0 -6
- package/toolkit/init.ts +0 -56
- package/toolkit/verify.ts +0 -19
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import tui from '../tui'
|
|
2
1
|
import { execa } from 'execa'
|
|
3
|
-
import { cp
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import
|
|
6
|
-
import { Compiler } from './base'
|
|
2
|
+
import { cp } from 'fs/promises'
|
|
3
|
+
import { join, parse } from 'path'
|
|
4
|
+
import tui from '../tui'
|
|
7
5
|
import type { Handler } from '../types'
|
|
6
|
+
import { nothing, readText, toolkitPrefix, writeText } from '../utils'
|
|
7
|
+
import { Compiler } from './base'
|
|
8
8
|
|
|
9
9
|
export class RunPython_Compiler extends Compiler {
|
|
10
10
|
id(): string {
|
|
@@ -35,30 +35,34 @@ export class RunPython_Compiler extends Compiler {
|
|
|
35
35
|
return ''
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
tool(): string {
|
|
39
|
+
return 'python3'
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
extension(): string {
|
|
39
43
|
return 'py'
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (handler.source_modifier === 'none') {
|
|
46
|
-
tui.command(`cp ${sourcePath} ${exePath}`)
|
|
47
|
-
await cp(join(directory, sourcePath), join(directory, exePath))
|
|
48
|
-
} else {
|
|
49
|
-
throw new Error(`Unknown source modifier: ${handler.source_modifier as string}`)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
tui.command(`python3 -m py_compile ${exePath}`)
|
|
46
|
+
override async compileNormal(handler: Handler, directory: string, sourcePath: string): Promise<string> {
|
|
47
|
+
tui.command(`python3 -m py_compile ${sourcePath}`)
|
|
53
48
|
|
|
54
49
|
const { exitCode } = await execa({
|
|
55
50
|
reject: false,
|
|
56
51
|
stderr: 'inherit',
|
|
57
52
|
stdout: 'inherit',
|
|
58
53
|
cwd: directory,
|
|
59
|
-
})`python3 -m py_compile ${
|
|
54
|
+
})`python3 -m py_compile ${sourcePath}`
|
|
60
55
|
|
|
61
|
-
if (exitCode !== 0)
|
|
56
|
+
if (exitCode !== 0) {
|
|
57
|
+
throw new Error(`Compilation failed for ${sourcePath}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sourcePath
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override async compileWithMain(handler: Handler, directory: string, sourcePath: string): Promise<string> {
|
|
64
|
+
await nothing()
|
|
65
|
+
throw new Error('Method not implemented.')
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
override async execute(
|
|
@@ -68,31 +72,21 @@ export class RunPython_Compiler extends Compiler {
|
|
|
68
72
|
inputPath: string,
|
|
69
73
|
outputPath: string,
|
|
70
74
|
): Promise<void> {
|
|
71
|
-
const
|
|
72
|
-
if (!(await exists(join(directory, exePath)))) {
|
|
73
|
-
throw new Error(`Executable file ${exePath} does not exist in directory ${directory}`)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const mergedPath = `solution-${inputPath}.py.exe`
|
|
77
|
-
|
|
78
|
-
tui.command(`Merging ${exePath} and ${inputPath} into ${mergedPath}`, { italic: true })
|
|
79
|
-
|
|
80
|
-
await this.mergeScripts(directory, exePath, inputPath, mergedPath)
|
|
81
|
-
|
|
82
|
-
const fullOutputPath = join(directory, outputPath)
|
|
75
|
+
const newSourcePath = `${toolkitPrefix()}-${parse(sourcePath).name}-${parse(inputPath).name}.py`
|
|
83
76
|
|
|
84
|
-
|
|
77
|
+
tui.command(`merge ${sourcePath} ${inputPath} > ${newSourcePath}`)
|
|
78
|
+
await this.mergeScripts(directory, sourcePath, inputPath, newSourcePath)
|
|
85
79
|
|
|
86
|
-
tui.command(`python3 ${
|
|
80
|
+
tui.command(`python3 ${newSourcePath} > ${outputPath}`)
|
|
87
81
|
|
|
88
82
|
const { exitCode } = await execa({
|
|
89
83
|
reject: false,
|
|
90
|
-
stdout: { file:
|
|
84
|
+
stdout: { file: join(directory, outputPath) },
|
|
91
85
|
stderr: 'inherit',
|
|
92
86
|
cwd: directory,
|
|
93
|
-
})`python3 ${
|
|
87
|
+
})`python3 ${newSourcePath}`
|
|
94
88
|
|
|
95
|
-
if (exitCode !== 0) throw new Error(`Execution failed for ${
|
|
89
|
+
if (exitCode !== 0) throw new Error(`Execution failed for ${newSourcePath}`)
|
|
96
90
|
}
|
|
97
91
|
|
|
98
92
|
async mergeScripts(
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Compiler } from './base'
|
|
2
|
+
|
|
3
|
+
export class Rust_Compiler extends Compiler {
|
|
4
|
+
id(): string {
|
|
5
|
+
return 'Rust'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
name(): string {
|
|
9
|
+
return 'Rust Compiler'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type(): string {
|
|
13
|
+
return 'compiler'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
language(): string {
|
|
17
|
+
return 'Rust'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async version(): Promise<string> {
|
|
21
|
+
return await this.getVersion('rustc --version', 0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
flags1(): string {
|
|
25
|
+
return '-C opt-level=2 -D warnings'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
flags2(): string {
|
|
29
|
+
return '-C opt-level=2 -D warnings'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tool(): string {
|
|
33
|
+
return 'rustc'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
extension(): string {
|
|
37
|
+
return 'rs'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import * as prompts from '@inquirer/prompts'
|
|
2
|
+
import { exists, mkdir } from 'fs/promises'
|
|
3
|
+
import Handlebars from 'handlebars'
|
|
4
|
+
import checkboxPlus from 'inquirer-checkbox-plus-plus'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import YAML from 'yaml'
|
|
7
|
+
import { ChatBot, cleanMardownCodeString, estimatePowerConsumption } from './ai'
|
|
8
|
+
import { languageNames, proglangNames } from './data'
|
|
9
|
+
import { getPromptForProglang, getTitleFromStatement } from './helpers'
|
|
10
|
+
import { settings } from './settings'
|
|
11
|
+
import tui from './tui'
|
|
12
|
+
import { Specification } from './types'
|
|
13
|
+
import { nothing, projectDir, readText, readYaml, writeTextInDir, writeYaml, writeYamlInDir } from './utils'
|
|
14
|
+
|
|
15
|
+
const promptsDir = join(projectDir(), 'assets', 'prompts')
|
|
16
|
+
|
|
17
|
+
const latexExample = await readText(join(promptsDir, 'examples', 'statement.tex'))
|
|
18
|
+
const statementCoda = await readText(join(promptsDir, 'examples', 'statement-coda.tex'))
|
|
19
|
+
const systemPrompt = await readText(join(promptsDir, 'creators', 'system-prompt.txt'))
|
|
20
|
+
const statementPromptTemplate = await readText(join(promptsDir, 'creators', 'create-statement.tpl.txt'))
|
|
21
|
+
const translationPromptTemplate = await readText(join(promptsDir, 'creators', 'create-translation.tpl.txt'))
|
|
22
|
+
const solutionPromptTemplate = await readText(join(promptsDir, 'creators', 'create-solution.tpl.txt'))
|
|
23
|
+
const sampleTestCasesPrompt = await readText(join(promptsDir, 'creators', 'sample-test-cases.txt'))
|
|
24
|
+
const privateTestCasesPrompt = await readText(join(promptsDir, 'creators', 'private-test-cases.txt'))
|
|
25
|
+
|
|
26
|
+
export async function createProblemWithJutgeAI(
|
|
27
|
+
model: string,
|
|
28
|
+
directory: string,
|
|
29
|
+
inputPath: string | undefined,
|
|
30
|
+
outputPath: string | undefined,
|
|
31
|
+
doNotAsk: boolean,
|
|
32
|
+
) {
|
|
33
|
+
if (await exists(directory)) {
|
|
34
|
+
throw new Error(`Directory ${directory} already exists`)
|
|
35
|
+
}
|
|
36
|
+
if (!directory.endsWith('.pbm')) {
|
|
37
|
+
throw new Error('The output directory must end with .pbm')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const spec = await getSpecification(inputPath, outputPath, doNotAsk)
|
|
41
|
+
const generator = new ProblemGenerator(spec, model)
|
|
42
|
+
await generator.run()
|
|
43
|
+
await generator.save(directory)
|
|
44
|
+
tui.success(`Created problem ${tui.hyperlink(directory)}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getSpecification(
|
|
48
|
+
inputPath: string | undefined,
|
|
49
|
+
outputPath: string | undefined,
|
|
50
|
+
doNotAsk: boolean,
|
|
51
|
+
): Promise<Specification> {
|
|
52
|
+
let spec: Specification
|
|
53
|
+
if (inputPath) {
|
|
54
|
+
tui.action(`Reading specification from ${inputPath}`)
|
|
55
|
+
const data = await readYaml(inputPath)
|
|
56
|
+
spec = Specification.parse(data)
|
|
57
|
+
} else {
|
|
58
|
+
doNotAsk = true
|
|
59
|
+
spec = {
|
|
60
|
+
title: 'Your new problem title',
|
|
61
|
+
description: 'Describe the task of the problem here.',
|
|
62
|
+
author: settings.name || 'Your Name',
|
|
63
|
+
email: settings.email || 'email@example.com',
|
|
64
|
+
golden_proglang: 'cc',
|
|
65
|
+
more_proglangs: ['py'],
|
|
66
|
+
original_language: 'en',
|
|
67
|
+
more_languages: ['ca', 'es'],
|
|
68
|
+
generators: ['hard', 'random', 'efficiency'],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (doNotAsk) {
|
|
73
|
+
return spec
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
spec.title = await prompts.input({ message: 'Title:', default: spec.title, prefill: 'editable' })
|
|
78
|
+
|
|
79
|
+
spec.description = await prompts.input({
|
|
80
|
+
message: 'Description:',
|
|
81
|
+
default: spec.description,
|
|
82
|
+
prefill: 'editable',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
spec.author = await prompts.input({ message: 'Author:', default: spec.author, prefill: 'editable' })
|
|
86
|
+
|
|
87
|
+
spec.email = await prompts.input({ message: 'Email:', default: spec.email, prefill: 'editable' })
|
|
88
|
+
|
|
89
|
+
spec.original_language = await prompts.select({
|
|
90
|
+
message: `Language for original version:`,
|
|
91
|
+
choices: Object.entries(languageNames).map(([value, name]) => ({ value, name })),
|
|
92
|
+
default: spec.original_language,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
spec.more_languages = await checkboxPlus({
|
|
96
|
+
message: `Other languages:`,
|
|
97
|
+
searchable: false,
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
99
|
+
source: async (answers, input) =>
|
|
100
|
+
Object.entries(languageNames)
|
|
101
|
+
.filter(([value]) => value !== spec.original_language)
|
|
102
|
+
.map(([value, name]) => ({ value, name })),
|
|
103
|
+
default: spec.more_languages,
|
|
104
|
+
loop: false,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
spec.golden_proglang = await prompts.select({
|
|
108
|
+
message: `Programming language for golden solution:`,
|
|
109
|
+
choices: Object.entries(proglangNames).map(([value, name]) => ({ value, name })),
|
|
110
|
+
default: spec.golden_proglang,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
spec.more_proglangs = await checkboxPlus({
|
|
114
|
+
message: `Other programming languages for solutions:`,
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
116
|
+
source: async (answers, input) =>
|
|
117
|
+
Object.entries(proglangNames)
|
|
118
|
+
.filter(([value]) => value !== spec.golden_proglang)
|
|
119
|
+
.map(([value, name]) => ({ value, name })),
|
|
120
|
+
default: spec.more_proglangs,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
spec.generators = await checkboxPlus({
|
|
124
|
+
message: `Test case generators:`,
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
126
|
+
source: async (answers, input) => [
|
|
127
|
+
{ value: 'random', name: 'Random' },
|
|
128
|
+
{ value: 'hard', name: 'Hard' },
|
|
129
|
+
{ value: 'efficiency', name: 'Efficiency' },
|
|
130
|
+
],
|
|
131
|
+
default: spec.generators,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const action = await prompts.select({
|
|
135
|
+
message: 'Choose an action:',
|
|
136
|
+
choices: [
|
|
137
|
+
{
|
|
138
|
+
value: 'confirm',
|
|
139
|
+
name: 'Confirm',
|
|
140
|
+
description: 'Confirm specification and proceed to problem creation',
|
|
141
|
+
},
|
|
142
|
+
{ value: 'edit', name: 'Edit', description: 'Edit the specification' },
|
|
143
|
+
{ value: 'cancel', name: 'Cancel', description: 'Cancel problem creation' },
|
|
144
|
+
],
|
|
145
|
+
})
|
|
146
|
+
if (action === 'confirm') break
|
|
147
|
+
if (action === 'cancel') {
|
|
148
|
+
tui.print('Problem creation cancelled')
|
|
149
|
+
process.exit(0)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (outputPath) {
|
|
154
|
+
await writeYaml(outputPath, spec)
|
|
155
|
+
tui.success(`Specification file written at ${tui.hyperlink(outputPath)}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return spec
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class ProblemGenerator {
|
|
162
|
+
private model: string
|
|
163
|
+
private spec: Specification
|
|
164
|
+
private bot: ChatBot
|
|
165
|
+
|
|
166
|
+
// generated problem parts
|
|
167
|
+
private problemStatement: string = ''
|
|
168
|
+
private problemMoreStatements: Record<string, string> = {}
|
|
169
|
+
private problemMoreSolutions: Record<string, string> = {}
|
|
170
|
+
private problemGenerators: Record<string, string> = {}
|
|
171
|
+
private problemSampleTests1: string = ''
|
|
172
|
+
private problemSampleTests2: string = ''
|
|
173
|
+
private problemPrivateTests1: string = ''
|
|
174
|
+
private problemPrivateTests2: string = ''
|
|
175
|
+
private problemReadme: string = ''
|
|
176
|
+
|
|
177
|
+
constructor(info: Specification, model: string) {
|
|
178
|
+
this.model = model
|
|
179
|
+
this.spec = info
|
|
180
|
+
this.bot = new ChatBot(model, systemPrompt)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async generateStatement(): Promise<string> {
|
|
184
|
+
return await tui.section(
|
|
185
|
+
`Generating problem statement in ${languageNames[this.spec.original_language]}`,
|
|
186
|
+
async () => {
|
|
187
|
+
const statementPrompt = Handlebars.compile(statementPromptTemplate)({
|
|
188
|
+
language: languageNames[this.spec.original_language],
|
|
189
|
+
latexExample,
|
|
190
|
+
title: this.spec.title,
|
|
191
|
+
description: this.spec.description,
|
|
192
|
+
})
|
|
193
|
+
const answer = await this.bot.complete(statementPrompt)
|
|
194
|
+
return cleanMardownCodeString(answer) + statementCoda
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async generateSampleTests(): Promise<string> {
|
|
200
|
+
return await tui.section('Generating sample test cases', async () => {
|
|
201
|
+
const answer = await this.bot.complete(sampleTestCasesPrompt)
|
|
202
|
+
return cleanMardownCodeString(answer)
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async generatePrivateTests(): Promise<string> {
|
|
207
|
+
return await tui.section('Generating private test cases', async () => {
|
|
208
|
+
const answer = await this.bot.complete(privateTestCasesPrompt)
|
|
209
|
+
return cleanMardownCodeString(answer)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async generateSolutions(): Promise<Record<string, string>> {
|
|
214
|
+
return await tui.section('Generating solutions', async () => {
|
|
215
|
+
const solutions: Record<string, string> = {}
|
|
216
|
+
for (const proglang of this.spec.more_proglangs.concat([this.spec.golden_proglang]).reverse()) {
|
|
217
|
+
await tui.section(`Generating solution in ${proglangNames[proglang]}`, async () => {
|
|
218
|
+
const proglangPrompt = await getPromptForProglang(proglang)
|
|
219
|
+
const solutionPrompt = Handlebars.compile(solutionPromptTemplate)({
|
|
220
|
+
proglang: proglangNames[proglang],
|
|
221
|
+
proglangPrompt,
|
|
222
|
+
})
|
|
223
|
+
const answer = await this.bot.complete(solutionPrompt)
|
|
224
|
+
solutions[proglang] = cleanMardownCodeString(answer)
|
|
225
|
+
this.bot.forgetLastInteraction()
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
return solutions
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async translateStatements(): Promise<Record<string, string>> {
|
|
233
|
+
return await tui.section('Translating problem statements', async () => {
|
|
234
|
+
const translations: Record<string, string> = {}
|
|
235
|
+
for (const language of this.spec.more_languages.sort()) {
|
|
236
|
+
await tui.section(`Translating to ${languageNames[language]}`, async () => {
|
|
237
|
+
const translationPrompt = Handlebars.compile(translationPromptTemplate)({
|
|
238
|
+
language: languageNames[language],
|
|
239
|
+
})
|
|
240
|
+
const answer = await this.bot.complete(translationPrompt)
|
|
241
|
+
translations[language] = cleanMardownCodeString(answer) + statementCoda
|
|
242
|
+
this.bot.forgetLastInteraction()
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
return translations
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async generateGenerators(): Promise<Record<string, string>> {
|
|
250
|
+
return await tui.section('Generating test cases generators', async () => {
|
|
251
|
+
const generators: Record<string, string> = {}
|
|
252
|
+
for (const type of this.spec.generators) {
|
|
253
|
+
await tui.section(`Generating ${type} test cases generator`, async () => {
|
|
254
|
+
const statement = this.problemStatement
|
|
255
|
+
const promptPath = join(projectDir(), 'assets', 'prompts', 'generators', `${type}.md`)
|
|
256
|
+
const promptTemplate = await readText(promptPath)
|
|
257
|
+
const prompt = Handlebars.compile(promptTemplate)({ statement })
|
|
258
|
+
const answer = cleanMardownCodeString(await this.bot.complete(prompt))
|
|
259
|
+
generators[type] = answer
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
return generators
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async generateReadme(): Promise<string> {
|
|
267
|
+
return tui.section('Generating README.md', async () => {
|
|
268
|
+
await nothing()
|
|
269
|
+
const readme = `
|
|
270
|
+
# Problem information
|
|
271
|
+
|
|
272
|
+
This programming problem for Jutge.org was generated by Jutge<sup>AI</sup> through the Jutge.org API using ${this.model} and a prompt by ${this.spec.author}.
|
|
273
|
+
|
|
274
|
+
**Warning**: This problem may contain inaccuracies or errors. Review the problem statements, test cases, and solutions carefully before using them in a real setting. Output tests and statement PDFs have not been generated, use \`jutge-make-problem\` to generate them.
|
|
275
|
+
|
|
276
|
+
## Author
|
|
277
|
+
|
|
278
|
+
${this.spec.author}
|
|
279
|
+
|
|
280
|
+
## Problem title
|
|
281
|
+
|
|
282
|
+
${this.spec.title}
|
|
283
|
+
|
|
284
|
+
## Problem description
|
|
285
|
+
|
|
286
|
+
${this.spec.description}
|
|
287
|
+
|
|
288
|
+
## Generated solutions
|
|
289
|
+
|
|
290
|
+
- ${proglangNames[this.spec.golden_proglang]} (golden solution)
|
|
291
|
+
${listify(this.spec.more_proglangs.map((proglang) => proglangNames[proglang]))}
|
|
292
|
+
|
|
293
|
+
## Generated languages
|
|
294
|
+
|
|
295
|
+
- ${languageNames[this.spec.original_language]} (original)
|
|
296
|
+
${listify(this.spec.more_languages.map((language) => languageNames[language]))}
|
|
297
|
+
|
|
298
|
+
## Generated test case generators
|
|
299
|
+
|
|
300
|
+
${listify(this.spec.generators)}
|
|
301
|
+
|
|
302
|
+
## Problem specification
|
|
303
|
+
|
|
304
|
+
\`\`\`yaml
|
|
305
|
+
${YAML.stringify(this.spec, null, 4)}
|
|
306
|
+
\`\`\`
|
|
307
|
+
|
|
308
|
+
## Model information
|
|
309
|
+
|
|
310
|
+
\`\`\`yaml
|
|
311
|
+
${YAML.stringify(this.bot.modelInformation(), null, 2)}
|
|
312
|
+
\`\`\`
|
|
313
|
+
|
|
314
|
+
## Estimated cost
|
|
315
|
+
|
|
316
|
+
The following information is based on estimations from token counts and do not reflect the actual costs incurred. Using GPT-5 pricing as reference.
|
|
317
|
+
|
|
318
|
+
- Total input tokens: ${this.bot.totalInputTokens}
|
|
319
|
+
- Total output tokens: ${this.bot.totalOutputTokens}
|
|
320
|
+
- Total input cost: ${this.bot.totalInputCost.toFixed(6)} USD
|
|
321
|
+
- Total output cost: ${this.bot.totalOutputCost.toFixed(6)} USD
|
|
322
|
+
- Total estimated cost: ${(this.bot.totalInputCost + this.bot.totalOutputCost).toFixed(6)} USD
|
|
323
|
+
- Energy: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).wattHours.toFixed(6)} Wh
|
|
324
|
+
- CO₂ emissions: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).co2Grams.toFixed(6)} g CO₂
|
|
325
|
+
|
|
326
|
+
`
|
|
327
|
+
return readme
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async run() {
|
|
332
|
+
await tui.section('Generating problem with JutgeAI', async () => {
|
|
333
|
+
await this.bot.init()
|
|
334
|
+
this.problemStatement = await this.generateStatement()
|
|
335
|
+
this.problemSampleTests1 = await this.generateSampleTests()
|
|
336
|
+
this.bot.forgetLastInteraction() // forget first sample tests
|
|
337
|
+
this.problemSampleTests2 = await this.generateSampleTests()
|
|
338
|
+
this.problemPrivateTests1 = await this.generatePrivateTests()
|
|
339
|
+
this.bot.forgetLastInteraction() // forget first private tests
|
|
340
|
+
this.problemPrivateTests2 = await this.generatePrivateTests()
|
|
341
|
+
this.problemMoreSolutions = await this.generateSolutions() // these are forgotten inside
|
|
342
|
+
this.bot.forgetLastInteraction() // forget private tests
|
|
343
|
+
this.bot.forgetLastInteraction() // forget sample tests
|
|
344
|
+
this.problemMoreStatements = await this.translateStatements()
|
|
345
|
+
this.bot.forgetLastInteraction() // forget translations
|
|
346
|
+
this.problemGenerators = await this.generateGenerators() // these are forgotten inside
|
|
347
|
+
this.problemReadme = await this.generateReadme()
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async save(path: string) {
|
|
352
|
+
await tui.section(`Saving problem to ${path}`, async () => {
|
|
353
|
+
await mkdir(path, { recursive: true })
|
|
354
|
+
await writeTextInDir(path, 'problem.en.tex', this.problemStatement)
|
|
355
|
+
|
|
356
|
+
const yml = {
|
|
357
|
+
title: this.spec.title,
|
|
358
|
+
author: this.spec.author,
|
|
359
|
+
email: this.spec.email,
|
|
360
|
+
model: this.model,
|
|
361
|
+
}
|
|
362
|
+
await writeYamlInDir(path, 'problem.en.yml', yml)
|
|
363
|
+
|
|
364
|
+
for (const [lang, translation] of Object.entries(this.problemMoreStatements)) {
|
|
365
|
+
await writeTextInDir(path, `problem.${lang}.tex`, translation)
|
|
366
|
+
const yml = {
|
|
367
|
+
title: getTitleFromStatement(translation) || this.spec.title,
|
|
368
|
+
translator: this.spec.author,
|
|
369
|
+
translator_email: this.spec.email,
|
|
370
|
+
original_language: 'en',
|
|
371
|
+
model: this.model,
|
|
372
|
+
}
|
|
373
|
+
await writeYamlInDir(path, `problem.${lang}.yml`, yml)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await writeTextInDir(path, 'sample-1.inp', this.problemSampleTests1)
|
|
377
|
+
await writeTextInDir(path, 'sample-2.inp', this.problemSampleTests2)
|
|
378
|
+
|
|
379
|
+
await writeTextInDir(path, 'test-1.inp', this.problemPrivateTests1)
|
|
380
|
+
await writeTextInDir(path, 'test-2.inp', this.problemPrivateTests2)
|
|
381
|
+
|
|
382
|
+
for (const [proglang, solution] of Object.entries(this.problemMoreSolutions)) {
|
|
383
|
+
const ext = proglang
|
|
384
|
+
await writeTextInDir(path, `solution.${ext}`, solution)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const [type, code] of Object.entries(this.problemGenerators)) {
|
|
388
|
+
await writeTextInDir(path, `generate-${type}.py`, code)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const handlerYml: any = {
|
|
392
|
+
handler: 'std',
|
|
393
|
+
solution: proglangNames[this.spec.golden_proglang],
|
|
394
|
+
}
|
|
395
|
+
await writeYamlInDir(path, 'handler.yml', handlerYml)
|
|
396
|
+
|
|
397
|
+
await writeTextInDir(path, 'README.md', this.problemReadme)
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function listify(items: (string | undefined)[]): string {
|
|
403
|
+
if (items.length === 0) {
|
|
404
|
+
return '<none>'
|
|
405
|
+
}
|
|
406
|
+
return items.map((item) => `- ${item}`).join('\n')
|
|
407
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { confirm, select, Separator } from '@inquirer/prompts'
|
|
2
|
+
import { cp, exists, glob } from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import tui from './tui'
|
|
5
|
+
import { projectDir, readText } from './utils'
|
|
6
|
+
import { title } from 'radash'
|
|
7
|
+
|
|
8
|
+
const templatesDir = path.join(projectDir(), 'assets', 'problems')
|
|
9
|
+
|
|
10
|
+
export async function selectTemplate(): Promise<string> {
|
|
11
|
+
const choices = []
|
|
12
|
+
|
|
13
|
+
const dirs = await Array.fromAsync(glob('*', { cwd: templatesDir }))
|
|
14
|
+
dirs.sort()
|
|
15
|
+
for (const dir of dirs) {
|
|
16
|
+
choices.push(new Separator(title(dir)))
|
|
17
|
+
const templates = await Array.fromAsync(glob('*.pbm', { cwd: path.join(templatesDir, dir) }))
|
|
18
|
+
templates.sort()
|
|
19
|
+
for (const template of templates) {
|
|
20
|
+
choices.push({ name: template, value: path.join(dir, template) })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let template: string | undefined = undefined
|
|
25
|
+
while (true) {
|
|
26
|
+
template = await select({ message: 'Select a template:', choices, default: template })
|
|
27
|
+
const readme = await readText(path.join(templatesDir, template, 'README.md'))
|
|
28
|
+
console.log()
|
|
29
|
+
await tui.markdown(readme)
|
|
30
|
+
const confirmation = await confirm({ message: `Use template ${template}?`, default: true })
|
|
31
|
+
if (confirmation) return template
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function createProblemWithTemplate(directory: string, template: string | undefined) {
|
|
36
|
+
if (await exists(directory)) {
|
|
37
|
+
throw new Error(`Directory ${directory} already exists`)
|
|
38
|
+
}
|
|
39
|
+
if (!directory.endsWith('.pbm')) {
|
|
40
|
+
throw new Error('The output directory must end with .pbm')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!template) {
|
|
44
|
+
template = await selectTemplate()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const templatePath = path.join(templatesDir, template)
|
|
48
|
+
|
|
49
|
+
if (!(await exists(templatePath))) {
|
|
50
|
+
throw new Error(`Template ${template} does not exist`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await cp(templatePath, directory, { recursive: true })
|
|
54
|
+
tui.success(`Created problem ${tui.hyperlink(directory)} from template ${template}`)
|
|
55
|
+
}
|
package/lib/data.ts
CHANGED
|
@@ -8,12 +8,18 @@ export const languageNames: Record<string, string> = {
|
|
|
8
8
|
de: 'German',
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export const languageKeys = Object.keys(languageNames)
|
|
12
|
+
|
|
11
13
|
export const proglangNames: Record<string, string> = {
|
|
12
14
|
c: 'C',
|
|
13
15
|
cc: 'C++',
|
|
14
16
|
py: 'Python3',
|
|
15
17
|
hs: 'Haskell',
|
|
16
18
|
clj: 'Clojure',
|
|
19
|
+
java: 'Java',
|
|
20
|
+
rs: 'Rust',
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export const proglangExtensions: Record<string, string> = invert(proglangNames)
|
|
24
|
+
|
|
25
|
+
export const proglangKeys = Object.keys(proglangNames)
|