Skip to content

Commit

Permalink
add --useAIForOperationId flag for konfig fix (#509)
Browse files Browse the repository at this point in the history
* add --useAIForOperationId flag for konfig fix

* docs(changeset): add --useAIForOperationId flag to konfig fix

* add param to fix-oas test

* add dummy key if env doesnt have openai key
  • Loading branch information
eddiechayes authored Jan 30, 2024
1 parent 15211ec commit f80a4f3
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 36 deletions.
5 changes: 5 additions & 0 deletions generator/konfig-dash/.changeset/fuzzy-trains-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'konfig-cli': patch
---

add --useAIForOperationId flag to konfig fix
1 change: 1 addition & 0 deletions generator/konfig-dash/packages/konfig-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"konfig-typescript-sdk": "workspace:*",
"lodash.merge": "^4.6.2",
"markdown-toc": "^1.2.0",
"openai": "^4.26.0",
"prettier": "^2.8.8",
"semver": "^7.3.8",
"shelljs": "^0.8.5",
Expand Down
10 changes: 9 additions & 1 deletion generator/konfig-dash/packages/konfig-cli/src/commands/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,15 @@ export default class Fix extends Command {
ci: Flags.boolean({
name: 'ci',
default: false,
description: 'Run in CI mode: answers default for all prompts if applicable',
description:
'Run in CI mode: answers default for all prompts if applicable',
}),
useAIForOperationId: Flags.boolean({
name: 'useAIForOperationId',
char: 'A',
default: false,
description:
'Use openAI API to generate operationIds based on rules defined here: https://konfigthis.com/docs/tutorials/naming-operation-ids. Requires OPENAI_API_KEY to be set in environment.',
}),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface FixOptions {
skipListUsageSecurity?: boolean
alwaysYes?: boolean
ci?: boolean
useAIForOperationId?: boolean
}

export async function executeFixCommand(options: FixOptions): Promise<void> {
Expand All @@ -32,6 +33,13 @@ export async function executeFixCommand(options: FixOptions): Promise<void> {
auto: options.auto ?? true,
alwaysYes: !!(options.alwaysYes == null || options.alwaysYes || options.ci), // ci mode should always confirm with yes
ci: options.ci ?? false,
useAIForOperationId: options.useAIForOperationId ?? false,
}

if (flags.useAIForOperationId && process.env.OPENAI_API_KEY === undefined) {
throw Error(
`OPENAI_API_KEY environment variable must be set to use AI for operationId`
)
}

const { parsedKonfigYaml } = parseKonfigYaml({
Expand Down Expand Up @@ -122,6 +130,7 @@ export async function executeFixCommand(options: FixOptions): Promise<void> {
ci: flags.ci,
skipMissingResponseDescription: flags.skipMissingResponseDescriptionFix,
skipFixListUsageSecurity: flags.skipListUsageSecurity,
useAIForOperationId: flags.useAIForOperationId,
})

fs.writeFileSync(
Expand Down
3 changes: 3 additions & 0 deletions generator/konfig-dash/packages/konfig-cli/src/util/fix-oas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function fixOas({
konfigYaml,
skipMissingResponseDescription,
skipFixListUsageSecurity,
useAIForOperationId,
}: {
spec: Spec
konfigYaml?: KonfigYamlType
Expand All @@ -52,6 +53,7 @@ export async function fixOas({
ci: boolean
skipMissingResponseDescription?: boolean
skipFixListUsageSecurity?: boolean
useAIForOperationId: boolean
}) {
/**
* ---Start fixing OAS---
Expand Down Expand Up @@ -106,6 +108,7 @@ export async function fixOas({
spec: spec.spec,
progress,
alwaysYes,
useAIForOperationId,
})

// Examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { operationIdSchema } from 'konfig-lib'
import { inquirerConfirm } from './inquirer-confirm'
import { logOperationDetails } from './log-operation-details'
import camelcase from 'camelcase'
import OpenAI from 'openai'

function updateOperationId({
operation,
Expand Down Expand Up @@ -68,16 +69,22 @@ export async function fixOperationIds({
spec,
progress,
alwaysYes,
useAIForOperationId,
}: {
spec: Spec['spec']
progress: Progress
alwaysYes: boolean
useAIForOperationId: boolean
}): Promise<number> {
let numberOfUpdatedOperationIds = 0
if (spec.paths === undefined) return numberOfUpdatedOperationIds
if (spec.tags === undefined || spec.tags.length === 0) {
throw Error('TODO')
}
// Dummy key prevents error from being thrown
const openai = new OpenAI({
apiKey: process.env['OPENAI_API_KEY'] ?? 'dummy',
})
const operations = getOperations({ spec })
for (const { operation, path, method } of operations) {
if (operation.operationId !== undefined) {
Expand All @@ -100,7 +107,7 @@ export async function fixOperationIds({
await updateOperationTag({ spec, operation, progress, path, method })
}
numberOfUpdatedOperationIds++
const { generateOperationId } = generatePrefix({ operation })
const { prefix, generateOperationId } = generatePrefix({ operation })
const operationsWithId = operations
.map((o) => o.operation)
.filter((operation) => operation.operationId !== undefined)
Expand Down Expand Up @@ -173,30 +180,80 @@ export async function fixOperationIds({
)
return suffix
}
const dflt = computeDefault()

const { suffix } = await inquirer.prompt<{ suffix: string }>([
{
type: 'input',
name: 'suffix',
message: `Enter operation ID:`,
default: dflt,
validate(suffix: string) {
if (suffix === '') return 'Please specify a non-empty string'
if (suffix.length < 3)
return 'Please enter a suffix longer than 2 characters'
const conflictingOperationId = operations
.map((o) => o.operation.operationId)
.find((operationId) => operationId === generateOperationId(suffix))
if (conflictingOperationId !== undefined)
return `Operation ID must be unique (conflicts with "${conflictingOperationId}")`
return true
},
transformer(suffix: string) {
return generateOperationId(suffix)

async function generateSuffixWithAI(): Promise<string> {
const existingIds = operations
.map((o) => o.operation.operationId)
.join(', ')
.slice(0, -2)
const prompt = `Generate an operation ID for this operation in my OpenAPI specification:
Path: ${path}
Method: ${method}
Summary: ${operation.summary}
Description: ${operation.description}
The operation ID must be prefixed with: ${prefix}, and MUST follow ALL of these rules:
1. The suffix must be at least 3 characters long.
2. The suffix must not redundantly re-use words from the prefix.
(For example: Store_get is preferred over Store_getStore.)
3. If the suffix contains both a verb and a noun, the noun should generally follow the verb. (For example, Store_placeOrder is preferred over Store_orderPlace).
4. The suffix must be in camelCase and CANNOT include any underscores.
Existing operation ids for this OpenAPI specification are as follows: ${existingIds}.
Based on these existing ids, the operation id must also follow these rules:
5. The operation ID must be unique.
6. If applicable, the same types of actions across different operations should be structured similarly. (For example, Cat_get, Dog_get, and Fish_get are preferred over Cat_get, Dog_retrieve, and Fish_fetch.)
ONLY RESPOND WITH JUST THE OPERATION ID ITSELF. Do not include any other text in your response.`

const response = (
await openai.chat.completions.create({
messages: [{ role: 'user', content: prompt }],
model: 'gpt-3.5-turbo',
})
).choices[0].message.content

if (response === undefined || response === null || response === '')
throw Error('OpenAI returned an empty response')

if (!response.startsWith(prefix))
throw Error(
`OpenAI returned an invalid response: "${response}; it should start with prefix "${prefix}"`
)
console.log(`AI-generated operation id: ${response}`)
return response.slice(prefix.length)
}

async function promptForSuffix(): Promise<{ suffix: string }> {
return await inquirer.prompt<{ suffix: string }>([
{
type: 'input',
name: 'suffix',
message: `Enter operation ID:`,
default: computeDefault(),
validate(suffix: string) {
if (suffix === '') return 'Please specify a non-empty string'
if (suffix.length < 3)
return 'Please enter a suffix longer than 2 characters'
const conflictingOperationId = operations
.map((o) => o.operation.operationId)
.find((opId) => opId === generateOperationId(suffix))
if (conflictingOperationId !== undefined)
return `Operation ID must be unique (conflicts with "${conflictingOperationId}")`
return true
},
transformer(suffix: string) {
return generateOperationId(suffix)
},
},
},
])
])
}

const suffix = useAIForOperationId
? await generateSuffixWithAI()
: (await promptForSuffix()).suffix

updateOperationId({
operation,
operationId: generateOperationId(suffix),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('fix-oas', () => {
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
Expand Down Expand Up @@ -95,7 +96,14 @@ describe('fix-oas', () => {
},
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})

Expand Down Expand Up @@ -138,7 +146,14 @@ describe('fix-oas', () => {
},
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
})
Expand Down Expand Up @@ -200,7 +215,14 @@ describe('fix-oas', () => {
},
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
})
Expand Down Expand Up @@ -244,7 +266,14 @@ describe('fix-oas', () => {
progress: {},
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
})
Expand Down Expand Up @@ -283,7 +312,14 @@ describe('fix-oas', () => {
progress: { ignorePotentialIncorrectType: true },
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
})
Expand Down Expand Up @@ -342,7 +378,14 @@ describe('fix-oas', () => {
progress: {},
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
})
Expand Down Expand Up @@ -399,7 +442,14 @@ describe('fix-oas', () => {
progress: { ignoreObjectsWithNoProperties: false },
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
it('generate schemas if ignored', async () => {
Expand All @@ -408,7 +458,14 @@ describe('fix-oas', () => {
progress: { ignoreObjectsWithNoProperties: true },
noSave: true,
})
await fixOas({ spec, progress, alwaysYes: true, auto: true, ci: false })
await fixOas({
spec,
progress,
alwaysYes: true,
auto: true,
ci: false,
useAIForOperationId: false,
})
expect(spec.spec).toMatchSnapshot()
})
})
Expand Down
Loading

0 comments on commit f80a4f3

Please sign in to comment.