@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.
Files changed (61) hide show
  1. package/assets/prompts/creators/create-solution.tpl.txt +10 -0
  2. package/assets/prompts/creators/create-statement.tpl.txt +21 -0
  3. package/assets/prompts/creators/create-translation.tpl.txt +5 -0
  4. package/assets/prompts/creators/private-test-cases.txt +6 -0
  5. package/assets/prompts/creators/sample-test-cases.txt +6 -0
  6. package/assets/prompts/creators/system-prompt.txt +2 -0
  7. package/assets/prompts/examples/statement-coda.tex +7 -0
  8. package/assets/prompts/examples/statement.tex +19 -0
  9. package/assets/prompts/generators/efficiency.md +41 -0
  10. package/assets/prompts/generators/hard.md +47 -0
  11. package/assets/prompts/generators/random.md +39 -0
  12. package/assets/prompts/proglangs/cc.md +3 -0
  13. package/assets/prompts/proglangs/py.md +40 -0
  14. package/lib/ai.ts +60 -4
  15. package/lib/cleaner.ts +24 -13
  16. package/lib/compilers/base.ts +70 -14
  17. package/lib/compilers/clojure.ts +21 -10
  18. package/lib/compilers/gcc.ts +4 -33
  19. package/lib/compilers/ghc.ts +4 -40
  20. package/lib/compilers/gxx.ts +4 -33
  21. package/lib/compilers/index.ts +9 -0
  22. package/lib/compilers/java.ts +105 -0
  23. package/lib/compilers/python3.ts +44 -37
  24. package/lib/compilers/run-clojure.ts +101 -0
  25. package/lib/compilers/run-haskell.ts +26 -22
  26. package/lib/compilers/run-python.ts +29 -35
  27. package/lib/compilers/rust.ts +39 -0
  28. package/lib/create-with-jutgeai.ts +407 -0
  29. package/lib/create-with-template.ts +55 -0
  30. package/lib/data.ts +6 -0
  31. package/lib/doctor.ts +86 -6
  32. package/lib/generate.ts +132 -290
  33. package/lib/helpers.ts +48 -0
  34. package/lib/inspector.ts +253 -0
  35. package/lib/jutge_api_client.ts +4631 -0
  36. package/lib/maker.ts +202 -289
  37. package/lib/settings.ts +26 -17
  38. package/lib/tui.ts +25 -15
  39. package/lib/types.ts +40 -5
  40. package/lib/upload.ts +216 -0
  41. package/lib/utils.ts +82 -14
  42. package/lib/versions.ts +46 -0
  43. package/package.json +50 -11
  44. package/toolkit/about.ts +43 -0
  45. package/toolkit/ai.ts +44 -18
  46. package/toolkit/check.ts +16 -0
  47. package/toolkit/clean.ts +16 -26
  48. package/toolkit/compilers.ts +4 -4
  49. package/toolkit/config.ts +91 -0
  50. package/toolkit/create.ts +30 -58
  51. package/toolkit/doctor.ts +15 -11
  52. package/toolkit/generate.ts +195 -98
  53. package/toolkit/index.ts +32 -21
  54. package/toolkit/make.ts +12 -48
  55. package/toolkit/upgrade.ts +9 -0
  56. package/toolkit/upload.ts +19 -0
  57. package/toolkit/create-jutge-ai.ts +0 -101
  58. package/toolkit/create-template.ts +0 -55
  59. package/toolkit/create-wizard.ts +0 -6
  60. package/toolkit/init.ts +0 -56
  61. package/toolkit/verify.ts +0 -19
package/lib/doctor.ts CHANGED
@@ -1,7 +1,7 @@
1
- import tui from './tui'
2
1
  import { execa } from 'execa'
3
- import { nothing } from './utils'
4
2
  import terminalLink from 'terminal-link'
3
+ import tui from './tui'
4
+ import { nothing } from './utils'
5
5
 
6
6
  export async function probePython3(showInfo: boolean = false): Promise<boolean> {
7
7
  if (showInfo) tui.command('python3 --version')
@@ -32,6 +32,38 @@ export async function probeGCC(showInfo: boolean = false): Promise<boolean> {
32
32
  return stdout.startsWith('Apple clang') || stdout.startsWith('g++')
33
33
  }
34
34
 
35
+ export async function probeHaskell(showInfo: boolean = false): Promise<boolean> {
36
+ if (showInfo) tui.command('ghc --version')
37
+ const { stdout } = await execa({ reject: false })`ghc --version`
38
+ const version = stdout.split('\n')[0]!.trim()
39
+ if (showInfo) console.log(version)
40
+ return stdout.startsWith('The Glorious Glasgow Haskell Compilation System')
41
+ }
42
+
43
+ export async function probeClojure(showInfo: boolean = false): Promise<boolean> {
44
+ if (showInfo) tui.command('clj --version')
45
+ const { stdout } = await execa({ reject: false })`clj --version`
46
+ const version = stdout.split('\n')[0]!.trim()
47
+ if (showInfo) console.log(version)
48
+ return stdout.startsWith('Clojure')
49
+ }
50
+
51
+ export async function probeJava(showInfo: boolean = false): Promise<boolean> {
52
+ if (showInfo) tui.command('javac -version')
53
+ const { stdout } = await execa({ reject: false })`javac -version`
54
+ const version = stdout.split('\n')[0]!.trim()
55
+ if (showInfo) console.log(version)
56
+ return stdout.startsWith('javac')
57
+ }
58
+
59
+ export async function probeRust(showInfo: boolean = false): Promise<boolean> {
60
+ if (showInfo) tui.command('rustc --version')
61
+ const { stdout } = await execa({ reject: false })`rustc --version`
62
+ const version = stdout.split('\n')[0]!.trim()
63
+ if (showInfo) console.log(version)
64
+ return stdout.startsWith('rustc')
65
+ }
66
+
35
67
  export async function probePdfLaTeX(showInfo: boolean = false): Promise<boolean> {
36
68
  if (showInfo) tui.command('pdflatex --version')
37
69
  const { stdout } = await execa({ reject: false })`pdflatex --version`
@@ -77,7 +109,6 @@ export async function checkPython3(): Promise<void> {
77
109
  tui.print('See https://www.python.org/downloads/')
78
110
  }
79
111
  }
80
-
81
112
  export async function checkGCC(): Promise<void> {
82
113
  if (await probeGCC(true)) {
83
114
  tui.success('C/C++ seems installed')
@@ -88,6 +119,46 @@ export async function checkGCC(): Promise<void> {
88
119
  }
89
120
  }
90
121
 
122
+ export async function checkHaskell(): Promise<void> {
123
+ if (await probeHaskell(true)) {
124
+ tui.success('Haskell seems installed')
125
+ } else {
126
+ tui.warning('Haskell does not appear to be installed')
127
+ tui.print('This is not a problem if you do not plan to use Haskell solutions')
128
+ tui.print('See https://www.haskell.org/ghc/download.html')
129
+ }
130
+ }
131
+
132
+ export async function checkClojure(): Promise<void> {
133
+ if (await probeClojure(true)) {
134
+ tui.success('Clojure seems installed')
135
+ } else {
136
+ tui.warning('Clojure does not appear to be installed')
137
+ tui.print('This is not a problem if you do not plan to use Clojure solutions')
138
+ tui.print('See https://clojure.org/guides/getting_started')
139
+ }
140
+ }
141
+
142
+ export async function checkJava(): Promise<void> {
143
+ if (await probeJava(true)) {
144
+ tui.success('Java seems installed')
145
+ } else {
146
+ tui.warning('Java does not appear to be installed')
147
+ tui.print('You will not be able to compile/execute Java solutions')
148
+ tui.print('See https://www.oracle.com/java/technologies/javase-jdk11-downloads.html')
149
+ }
150
+ }
151
+
152
+ export async function checkRust(): Promise<void> {
153
+ if (await probeRust(true)) {
154
+ tui.success('Rust seems installed')
155
+ } else {
156
+ tui.warning('Rust does not appear to be installed')
157
+ tui.print('You will not be able to compile/execute Rust solutions')
158
+ tui.print('See https://www.rust-lang.org/tools/install')
159
+ }
160
+ }
161
+
91
162
  export async function checkPdfLaTeX(): Promise<void> {
92
163
  if (await probePdfLaTeX(true)) {
93
164
  tui.success('LaTeX seems installed')
@@ -128,14 +199,14 @@ export async function checkImageMagick(): Promise<void> {
128
199
  }
129
200
  }
130
201
 
131
- export async function checkEnvVars(): Promise<void> {
202
+ export async function checkAIEnvVars(): Promise<void> {
132
203
  await nothing()
133
204
  const vars = ['OPENAI_API_KEY', 'GEMINI_API_KEY']
134
205
  for (const v of vars) {
135
206
  if (process.env[v]) {
136
- tui.success(`${v} is set`)
207
+ tui.success(`${v} environment variable is set`)
137
208
  } else {
138
- tui.warning(`${v} is not set`)
209
+ tui.warning(`${v} environment variable is not set`)
139
210
  tui.print(`You will not be able to use related AI models`)
140
211
  }
141
212
  }
@@ -155,4 +226,13 @@ export async function checkTerminal(): Promise<void> {
155
226
  tui.warning('Terminal does not support hyperlinks')
156
227
  tui.print('Links to files may not be clickable')
157
228
  }
229
+
230
+ const vars = ['EDITOR', 'VISUAL']
231
+ for (const v of vars) {
232
+ if (process.env[v]) {
233
+ tui.success(`${v} environment variable is set to "${process.env[v]}"`)
234
+ } else {
235
+ tui.warning(`${v} environment variable is not set`)
236
+ }
237
+ }
158
238
  }
package/lib/generate.ts CHANGED
@@ -1,329 +1,171 @@
1
- import { ChatBot, cleanMardownCodeString, estimatePowerConsumption } from './ai'
2
- import tui from './tui'
3
- import chalk from 'chalk'
4
- import { appendFile, mkdir } from 'fs/promises'
5
- import { oraPromise } from 'ora'
1
+ import Handlebars from 'handlebars'
6
2
  import { join } from 'path'
7
- import { z } from 'zod'
8
- import { paths } from './settings'
9
- import { proglangNames, languageNames } from './data'
10
- import { nothing, writeText, writeYaml } from './utils'
11
- import YAML from 'yaml'
12
-
13
- const showPrompt = false
14
- const showAnswer = false
15
-
16
- export const ZProblemData = z.object({
17
- title: z.string(),
18
- description: z.string(),
19
- author: z.string(),
20
- email: z.string().email(),
21
- type: z.string(),
22
- proglangs: z.array(z.string()),
23
- languages: z.array(z.string()),
24
- model: z.string(),
25
- outputDir: z.string(),
26
- })
27
-
28
- export type ProblemData = z.infer<typeof ZProblemData>
29
-
30
- export async function generateProblemWithJutgeAI(info: ProblemData): Promise<void> {
31
- const generator = new ProblemGenerator(info)
32
- await generator.run()
33
- await generator.save(info.outputDir)
34
- }
35
-
36
- class ProblemGenerator {
37
- private bot: ChatBot
38
- private info: ProblemData
39
-
40
- // generated problem parts
41
- private problemStatement: string = ''
42
- private problemSampleTests: string = ''
43
- private problemPrivateTests: string = ''
44
- private problemSolutions: Record<string, string> = {}
45
- private problemTranslations: Record<string, string> = {}
46
- private problemReadme: string = ''
47
-
48
- private latexExample = `
49
- \\Problem{Which one is missing?}
50
-
51
- \\Statement
52
-
53
- Johny has a list of objects, labeled between 1 and $n$, but he lost one of them. He wants to know which one is missing.
54
-
55
- \\medskip
56
-
57
- Write a program that reads sequences with all the numbers between 1 and $n$ but one, and tells which one is missing.
58
-
59
- \\Input
60
-
61
- Input consists of several sequences.
62
- Every sequence begins with a number $n$ between~1 and~$10^4$ followed by $n - 1$ natural numbers.
63
- Every number between 1 and $n$ appears exactly once, except one of them, which is missing.
64
-
65
- \\Output
66
-
67
- For every sequence, print the missing number.
68
- `
69
-
70
- private systemPrompt = `
71
- You are an expert in coding and writing programmming problems.
72
- You provide accurate information and write following the instructed format.
73
- `
74
-
75
- private statementCoda = `
76
-
77
- \\Observation
78
-
79
- This problem has been generated by Jutge$^{\\text{AI}}$. Remove this observation after reviewing it carefully.
3
+ import { cleanMardownCodeString, complete } from './ai'
4
+ import { languageKeys, languageNames, proglangExtensions, proglangKeys, proglangNames } from './data'
5
+ import { getPromptForProglang, getTitleFromStatement } from './helpers'
6
+ import type { Inspector } from './inspector'
7
+ import { settings } from './settings'
8
+ import tui from './tui'
9
+ import { projectDir, readText, readTextInDir, stripLaTeX, writeTextInDir, writeYamlInDir } from './utils'
10
+ import { exists } from 'fs/promises'
80
11
 
81
- \\Sample
12
+ export async function addStatementTranslation(model: string, inspector: Inspector, language: string) {
13
+ const original = inspector.originalLanguage
82
14
 
83
- `
84
- constructor(info: ProblemData) {
85
- this.info = info
86
- this.bot = new ChatBot(info.model, this.systemPrompt)
15
+ if (!original) {
16
+ throw new Error('Original language not set, cannot generate translations')
17
+ }
18
+ if (language === original) {
19
+ throw new Error(`Language ${language} is the original language`)
20
+ }
21
+ if (inspector.languages.includes(language)) {
22
+ throw new Error(`Language ${language} already exists`)
23
+ }
24
+ if (!languageKeys.includes(language)) {
25
+ throw new Error(`Language ${language} is not supported`)
87
26
  }
88
27
 
89
- async generateStatement(): Promise<string> {
90
- return await tui.section('Generating problem statement', async () => {
91
- const statementPrompt = `
92
- You are to write the statement of a programming problem
93
-
94
- The statement must be written in LaTeX, using a few predefined macros.
95
- Use LaTeX math syntax for formulas and variables.
96
- Use dollars for inline maths and use \\[ and \\] for display maths.
97
- Do not add input/output test cases in the statement.
98
- Separate paragraphs by a blank line and \\medskip macro.
99
-
100
- Write in the style of Salvador Roura, the famous problem setter from Jutge.org.
101
-
102
- Here is an example for an unrelated problem, follow its structure and macros:
103
-
104
- ${this.latexExample}
105
-
106
- The title for this problem is:
28
+ const statememt = await readTextInDir(inspector.directory, `problem.${original}.tex`)
107
29
 
108
- ${this.info.title}
30
+ await tui.section(`Translating from ${languageNames[original]} to ${languageNames[language]}`, async () => {
31
+ const prompt = `
32
+ Translate the given problem statement to ${languageNames[language]}.
109
33
 
110
- Here is the description of this problem and some additional instructions:
34
+ The translation must be accurate and use proper technical terminology.
35
+ Maintain the LaTeX formatting and macros.
36
+ The texts that the program must read and write should not be translated.
37
+ `
38
+
39
+ const answer = cleanMardownCodeString(await complete(model, prompt, statememt))
40
+
41
+ const info = {
42
+ title: getTitleFromStatement(answer) || 'Error translating title',
43
+ translator: settings.name,
44
+ translator_email: settings.email,
45
+ model,
46
+ }
47
+ await writeTextInDir(inspector.directory, `problem.${language}.tex`, answer)
48
+ await writeYamlInDir(inspector.directory, `problem.${language}.yml`, info)
49
+
50
+ tui.success(`Translation completed: files problem.${language}.tex and problem.${language}.yml created`)
51
+ tui.warning(`Please review the generated translation`)
52
+ })
53
+ }
111
54
 
112
- ${this.info.description}
113
- `
114
- const answer = cleanMardownCodeString(await this.complete(statementPrompt)) + this.statementCoda
115
- return answer
116
- })
55
+ export async function addAlternativeSolution(model: string, inspector: Inspector, extension: string) {
56
+ if (!inspector.goldenSolution) {
57
+ throw new Error('No golden solution defined')
117
58
  }
118
-
119
- async generateSampleTests(): Promise<string> {
120
- return await tui.section('Generating sample test cases', async () => {
121
- const sampleTestCasesPrompt = `
122
- Now, write a sample test case file to illustrate the input that the program must read according to the problem statement.
123
-
124
- Only the input should be given, the output will be computed from it.
125
- Sample test cases should be relatively small but cover interesting cases but not all edge cases.
126
- `
127
- const answer = cleanMardownCodeString(await this.complete(sampleTestCasesPrompt))
128
- return answer
129
- })
59
+ const originalExtension = inspector.goldenSolution.split('.').pop()
60
+ if (!originalExtension) {
61
+ throw new Error('Invalid golden solution filename')
130
62
  }
131
-
132
- async generatePrivateTests(): Promise<string> {
133
- return await tui.section('Generating private test cases', async () => {
134
- const privateTestCasesPrompt = `
135
- Now, write a private test case file to check the correctness and efficiency of the program.
136
-
137
- Only the input should be given, the output will be computed from it.
138
- The private test cases should cover edge cases.
139
- Limit the number of test cases to 20.
140
- `
141
- const answer = cleanMardownCodeString(await this.complete(privateTestCasesPrompt))
142
- return answer
143
- })
63
+ if (!proglangKeys.includes(extension)) {
64
+ throw new Error(`Extension ${extension} is not supported`)
65
+ }
66
+ if (inspector.solutions.includes(`solution.${extension}`)) {
67
+ throw new Error(`Solution already exists`)
144
68
  }
69
+ const proglang = proglangNames[extension]
145
70
 
146
- async generateSolutions(): Promise<Record<string, string>> {
147
- return await tui.section('Generating solutions', async () => {
148
- const solutions: Record<string, string> = {}
149
- for (const proglang of this.info.proglangs) {
150
- await tui.section(`Generating solution in ${proglangNames[proglang]}`, async () => {
151
- const solutionPrompt = `
152
- Now, write a solution in ${proglangNames[proglang]} to solve this problem
153
-
154
- It must be written in an idiomatic way and only include relevant comments.
155
- The code must be efficient and handle all edge cases.
156
- The code must read from standard input and write to standard output.
157
- The code must be well written and easy to understand to novices.
158
- The code does not have to check the preconditions stated in the probleM
159
- Do not use any non-standard libraries.
160
- ${proglang === 'cpp' ? 'Do not use fast input/output methods.' : ''}
161
- ${proglang === 'cpp' ? 'Add a using namespace std; declaration after the includes and do not use std:: prefixes.' : ''}
162
- ${proglang === 'py' ? 'Use type annotations.' : ''}
163
- `
164
- const answer = cleanMardownCodeString(await this.complete(solutionPrompt))
165
- this.bot.forgetLastInteraction()
166
- solutions[proglang] = answer
167
- })
168
- }
169
- return solutions
170
- })
71
+ if (!proglang) {
72
+ throw new Error(`Programming language for extension ${extension} not found`)
171
73
  }
172
74
 
173
- async translateStatements(): Promise<Record<string, string>> {
174
- return await tui.section('Translating problem statements', async () => {
175
- const translations: Record<string, string> = {}
176
- for (const language of this.info.languages) {
177
- await tui.section(`Translating to ${languageNames[language]}`, async () => {
178
- const translationPrompt = `
179
- Now, translate the problem statement to ${languageNames[language]}.
75
+ const originalProglang = proglangNames[originalExtension]
180
76
 
181
- The translation must be accurate and use proper technical terminology.
182
- Maintain the LaTeX formatting and macros.
183
- The texts that the program must read and write should not be translated.
184
- `
185
- const answer = cleanMardownCodeString(await this.complete(translationPrompt)) + this.statementCoda
186
- this.bot.forgetLastInteraction()
187
- translations[language] = answer
188
- })
189
- }
190
- return translations
191
- })
192
- }
77
+ const goldenSource = await readTextInDir(inspector.directory, inspector.goldenSolution)
193
78
 
194
- async generateReadme(): Promise<string> {
195
- return tui.section('Generating README.md', async () => {
196
- await nothing()
197
- const readme = `
198
- # ${this.info.title}
79
+ const proglangPrompt = await getPromptForProglang(proglang)
199
80
 
200
- This programming problem for Jutge.org was generated by Jutge<sup>AI</sup> through the Jutge.org API using ${this.info.model} and a prompt by ${this.info.author}.
81
+ const prompt = `
82
+ Convert the given program in ${originalProglang} to ${proglang}.
201
83
 
202
- **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.
84
+ The translation must be accurate and follow the structure and logic of the original program.
85
+ Each function should have a documentation comment explaining its purpose, parameters, and return values.
203
86
 
204
- ## Author
87
+ If some function in the original program is not defined, do not define it in the translated program either.
205
88
 
206
- ${this.info.author}
89
+ Your program should start with a comment line saying 'Generated by ${model}'.
207
90
 
208
- ## Original prompt
91
+ ${proglangPrompt}
92
+ `
209
93
 
210
- ${this.info.description}
94
+ const answer = cleanMardownCodeString(await complete(model, prompt, goldenSource))
211
95
 
212
- ## Generated solutions
96
+ await writeTextInDir(inspector.directory, `solution.${extension}`, answer)
213
97
 
214
- - ${this.info.proglangs.map((proglang) => proglangNames[proglang]).join('\n- ')}
98
+ tui.success(`solution.${extension} created`)
99
+ tui.warning(`Please review the generated solution`)
100
+ }
215
101
 
216
- ## Generated languages
102
+ // TODO: check this function
103
+ export async function addMainFile(model: string, inspector: Inspector, proglang: string) {
104
+ if (!inspector.goldenSolution) {
105
+ throw new Error('No golden solution defined')
106
+ }
217
107
 
218
- - English
219
- - ${this.info.languages.map((language) => languageNames[language]).join('\n- ')}
108
+ const originalExtension = inspector.goldenSolution.split('.').pop()
109
+ if (!originalExtension) {
110
+ throw new Error('Invalid golden solution filename')
111
+ }
112
+ const originalProglang = proglangNames[originalExtension]
220
113
 
221
- ## Model information
114
+ const goldenSource = await readTextInDir(inspector.directory, `main.${originalExtension}`)
222
115
 
223
- \`\`\`yaml
224
- ${YAML.stringify(this.bot.modelInformation(), null, 2)}
225
- \`\`\`
116
+ await tui.section(`Converting main.${originalExtension} to ${proglang}`, async () => {
117
+ const proglangPrompt = await getPromptForProglang(proglang)
226
118
 
227
- ## Estimated cost
119
+ const prompt = `
120
+ Convert the given program in ${originalProglang} to ${proglang}.
228
121
 
229
- The following information is based on estimations from token counts and do not reflect the actual costs incurred. Using GPT-5 pricing as reference.
122
+ The translation must be accurate and follow the structure and logic of the original program.
230
123
 
231
- - Total input tokens: ${this.bot.totalInputTokens}
232
- - Total output tokens: ${this.bot.totalOutputTokens}
233
- - Total input cost: ${this.bot.totalInputCost.toFixed(6)} USD
234
- - Total output cost: ${this.bot.totalOutputCost.toFixed(6)} USD
235
- - Total estimated cost: ${(this.bot.totalInputCost + this.bot.totalOutputCost).toFixed(6)} USD
236
- - Energy: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).wattHours.toFixed(6)} Wh
237
- - CO₂ emissions: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).co2Grams.toFixed(6)} g CO₂
124
+ If some function in the original program is not defined, do not define it in the translated program either.
238
125
 
239
- `
240
- return readme
241
- })
242
- }
126
+ Your program should start with a comment line saying 'Generated by ${model}'.
243
127
 
244
- async run() {
245
- tui.title('Generating problem with JutgeAI')
246
- await this.bot.init()
247
- this.problemStatement = await this.generateStatement()
248
- this.problemSampleTests = await this.generateSampleTests()
249
- this.problemPrivateTests = await this.generatePrivateTests()
250
- this.problemSolutions = await this.generateSolutions() // these are forgotten inside
251
- this.bot.forgetLastInteraction() // forget private tests
252
- this.bot.forgetLastInteraction() // forget sample tests
253
- this.problemTranslations = await this.translateStatements()
254
- this.problemReadme = await this.generateReadme()
255
- }
128
+ ${proglangPrompt}
129
+ `
256
130
 
257
- async save(path: string) {
258
- await tui.section(`Saving problem to ${path}`, async () => {
259
- await mkdir(path, { recursive: true })
260
- await writeText(join(path, 'problem.en.tex'), this.problemStatement)
261
-
262
- const yml = {
263
- title: this.info.title,
264
- author: this.info.author,
265
- email: this.info.email,
266
- model: this.info.model,
267
- }
268
- await writeYaml(`${path}/problem.en.yml`, yml)
269
-
270
- for (const [lang, translation] of Object.entries(this.problemTranslations)) {
271
- await writeText(join(path, `problem.${lang}.tex`), translation)
272
- const yml = {
273
- title: getTitleFromStatement(translation) || this.info.title,
274
- translator: this.info.author,
275
- translator_email: this.info.email,
276
- original_language: 'en',
277
- model: this.info.model,
278
- }
279
- await writeYaml(join(path, `problem.${lang}.yml`), yml)
280
- }
281
-
282
- await writeText(join(path, 'sample.inp'), this.problemSampleTests)
283
-
284
- await writeText(join(path, 'test.inp'), this.problemPrivateTests)
285
-
286
- for (const [proglang, solution] of Object.entries(this.problemSolutions)) {
287
- const ext = proglang
288
- await writeText(join(path, `solution.${ext}`), solution)
289
- }
290
-
291
- const handlerYml: any = {
292
- handler: 'std',
293
- }
294
- await writeYaml(join(path, 'handler.yml'), handlerYml)
295
-
296
- await writeText(join(path, 'README.md'), this.problemReadme)
297
- })
298
- }
131
+ const answer = cleanMardownCodeString(await complete(model, prompt, goldenSource))
299
132
 
300
- async complete(prompt: string): Promise<string> {
301
- if (showPrompt) console.log(chalk.gray(prompt))
302
- await appendFile(join(paths.log, 'jutge-ai.log'), '------------------- PROMPT -------------------\n\n')
303
- await appendFile(join(paths.log, 'jutge-ai.log'), prompt + '\n\n')
304
-
305
- const answer = await oraPromise(
306
- async () => {
307
- return await this.bot.complete(prompt)
308
- },
309
- {
310
- text: `JutgeAI is thinking`,
311
- },
312
- )
313
-
314
- if (showAnswer) console.log(chalk.gray(answer))
315
- await appendFile(join(paths.log, 'jutge-ai.log'), '------------------- ANSWER -------------------\n\n')
316
- await appendFile(join(paths.log, 'jutge-ai.log'), answer + '\n\n')
317
-
318
- return answer
319
- }
133
+ await writeTextInDir(inspector.directory, `main.${proglangExtensions[proglang]}`, answer)
134
+
135
+ tui.success(`main.${proglang} created`)
136
+ tui.warning(`Please review the generated file`)
137
+ })
138
+ }
139
+
140
+ export async function generateTestCasesGenerator(
141
+ model: string,
142
+ inspector: Inspector,
143
+ output: string, // file to create (template with {{type}})
144
+ type: 'random' | 'hard' | 'efficiency',
145
+ ) {
146
+ const outputPath = Handlebars.compile(output)({ type })
147
+ await tui.section(`Generating test cases generator ${outputPath}`, async () => {
148
+ const statement = await getStatementAsText(inspector)
149
+ const promptPath = join(projectDir(), 'assets', 'prompts', 'generators', `${type}.md`)
150
+ const promptTemplate = await readText(promptPath)
151
+ const prompt = Handlebars.compile(promptTemplate)({ statement })
152
+ const answer = cleanMardownCodeString(await complete(model, '', prompt))
153
+ await writeTextInDir(inspector.directory, outputPath, answer)
154
+ tui.success(`Test cases generator ${outputPath} generated`)
155
+ tui.warning(`Please review the generated test cases generator`)
156
+ })
320
157
  }
321
158
 
322
- export function getTitleFromStatement(statement: string): string | null {
323
- const pattern = /\\Problem\{(.*?)\}/
324
- const match = statement.match(pattern)
325
- if (match) {
326
- return match[1]!.trim()
159
+ // TODO: move into inspector?
160
+ export async function getStatementAsText(inspector: Inspector): Promise<string> {
161
+ const original = inspector.originalLanguage
162
+ if (!original) throw new Error('Original language not set')
163
+ if (await exists(join(inspector.directory, `problem.${original}.txt`))) {
164
+ const text = await readTextInDir(inspector.directory, `problem.${original}.txt`)
165
+ return text
166
+ } else {
167
+ const latex = await readTextInDir(inspector.directory, `problem.${original}.tex`)
168
+ const text = stripLaTeX(latex)
169
+ return text
327
170
  }
328
- return null
329
171
  }
package/lib/helpers.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { exists } from 'fs/promises'
2
+ import { projectDir, readText } from './utils'
3
+ import { join } from 'path'
4
+ import { normalize, resolve } from 'node:path'
5
+ import { languageNames } from './data'
6
+
7
+ export function getTitleFromStatement(statement: string): string | null {
8
+ const pattern = /\\Problem\{(.*?)\}/
9
+ const match = statement.match(pattern)
10
+ if (match) {
11
+ return match[1]!.trim()
12
+ }
13
+ return null
14
+ }
15
+
16
+ export async function getPromptForProglang(proglang: string): Promise<string> {
17
+ const location = join(projectDir(), 'assets', 'prompts', `${proglang}.md`)
18
+ if (await exists(location)) {
19
+ return await readText(location)
20
+ } else {
21
+ return ''
22
+ }
23
+ }
24
+
25
+ /*
26
+ Find real problem directories from a list of directories. A real problem directory is one that contains a handler.yml file.
27
+ If a directory does not contain a handler.yml file, check if it contains subdirectories for each language that contain a handler.yml file.
28
+ If so, add those subdirectories to the list of real problem directories.
29
+ */
30
+ export async function findRealDirectories(directories: string[]): Promise<string[]> {
31
+ const realDirectories: string[] = []
32
+ for (let dir of directories) {
33
+ if (dir === '.') {
34
+ dir = normalize(resolve(process.cwd()))
35
+ }
36
+ if (await exists(join(dir, 'handler.yml'))) {
37
+ realDirectories.push(dir)
38
+ } else {
39
+ for (const language of Object.keys(languageNames).sort()) {
40
+ const child = join(dir, language, 'handler.yml')
41
+ if (await exists(child)) {
42
+ realDirectories.push(resolve(dir, language))
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return realDirectories
48
+ }