Skip to content

Commit

Permalink
feat: add support for inferring the benefit and category for a single…
Browse files Browse the repository at this point in the history
… claim with OpenAI
  • Loading branch information
timrogers committed Jul 30, 2023
1 parent 6bb57c3 commit 41f59b1
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 15 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ To remember your login, Formanator stores a `.formanator.json` file in your home

You can quickly submit a single claim from the command line.

When submitting a claim, you need to specify a benefit and category for your claim. You can either decide that yourself, or you can have OpenAI do it for you, for a cost of about $0.003 per claim 🧠

#### Submitting a single claim with AI magic

1. Set up an OpenAI account and make sure you either (a) have free trial credit available or (b) have set up a payment method. You can check this on the ["Usage"](https://platform.openai.com/account/usage) page.
2. Create an [OpenAI API key](https://platform.openai.com/account/api-keys). Set the API key as the `OPENAI_API_KEY` environment variable.
3. Figure out what you're planning to claim for.
4. Make sure you're logged in - for more details, see "Connecting to your Forma account" above.
5. Submit your claim by running `formanator submit-claim`. You'll need to pass a bunch of arguments:

```bash
formanator submit-claim --amount 2.28 \
--merchant Amazon \
--description "USB cable" \
--purchase-date 2023-01-15 \
--receipt-path "USB.pdf"
```

6. We'll use OpenAI to figure out the benefit and category for your claim, and give you the chance to check the result.
7. If you confirm the benefit and category by hitting Enter, your claim will be submitted.

#### Submitting a single claim, specifying the benefit and category yourself

1. Figure out what you're planning to claim for.
2. Make sure you're logged in - for more details, see "Connecting to your Forma account" above.
3. Get a list of your available benefits by running `formanator benefits`.
Expand All @@ -56,7 +79,7 @@ You can submit multiple claims at once by generating a template CSV, filling it

1. Make sure you're logged in - for more details, see "Connecting to your Forma account" above.
2. Run `formanator generate-template-csv` to generate a CSV template. By default, the template will be saved as `claims.csv`. Optionally, you can specify the `--output-path` argument to choose where to save the template.
3. Update the template, filling in the columns for each of your claims. To get valid `benefit` and `category` values, use the `formanator benefits` and `formanator categories --benefit <benefit>` commandsa documented in "Submitting a single claim" above.
3. Update the template, filling in the columns for each of your claims. To get valid `benefit` and `category` values, use the `formanator benefits` and `formanator categories --benefit <benefit>` commands documented in "Submitting a single claim, specifying the benefit and category yourself" above.
4. Submit your claims by running `formanator submit-claims-from-csv --input-path claims.csv`.
5. Your claims will be submitted. If there are any validation errors with any of the rows, or if anything goes wrong during submission, an error message will be displayed, but the tool will continue submitting other claims.

Expand Down
61 changes: 55 additions & 6 deletions dist/commands/submit-claim.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,77 @@
import * as commander from 'commander';
import chalk from 'chalk';
import { actionRunner } from '../utils.js';
import { Configuration, OpenAIApi } from 'openai';
import { actionRunner, prompt } from '../utils.js';
import { getAccessToken } from '../config.js';
import { createClaim } from '../forma.js';
import { createClaim, getBenefitsWithCategories, } from '../forma.js';
import { claimParamsToCreateClaimOptions } from '../claims.js';
const command = new commander.Command();
const attemptToinferCategoryAndBenefit = async (opts) => {
const configuration = new Configuration({
apiKey: opts.openaiApiKey,
});
const openai = new OpenAIApi(configuration);
const categoriesWithBenefits = opts.benefitsWithCategories.flatMap((benefit) => benefit.categories.map((category) => ({ ...category, benefit })));
const categoryNames = categoriesWithBenefits.flatMap((category) => category.subcategory_alias ?? category.subcategory_name);
const content = `Your job is to predict the category for an expense claim based on the name of the merchant and a description of what was purchased. You should give a single, specific answer without any extra words or punctuation.\n\nHere are the possible categories:\n\n${categoryNames.join('\n')}\n\nPlease predict the category for the following example claim:\nMerchant: ${opts.merchant}\nDescription:${opts.description}`;
const chatCompletion = await openai.createChatCompletion({
model: 'gpt-3.5-turbo-16k',
messages: [
{
role: 'user',
content,
},
],
});
const returnedCategoryAsString = chatCompletion.data.choices[0].message?.content;
if (!returnedCategoryAsString) {
throw new Error(`Something went wrong while inferring the benefit and category for your claim. OpenAI returned an unexpected response: ${JSON.stringify(chatCompletion.data)}`);
}
const returnedCategory = categoriesWithBenefits.find((category) => category.subcategory_alias === returnedCategoryAsString ||
category.subcategory_name === returnedCategoryAsString);
if (!returnedCategory) {
throw new Error(`Something went wrong while inferring the benefit and category for your claim. OpenAI returned a response that wasn't a valid category: ${returnedCategoryAsString}`);
}
console.log(chalk.green(`OpenAI inferred that you should claim using the "${returnedCategory.benefit.name}" benefit and "${returnedCategoryAsString}" category. If that seems right, hit Enter. If not, press Ctrl + C to end your session.`));
prompt('> ');
return { category: returnedCategoryAsString, benefit: returnedCategory.benefit.name };
};
command
.name('submit-claim')
.description('Submit a claim for a Forma benefit')
.requiredOption('--benefit <benefit>', 'The benefit you are claiming for')
.option('--benefit <benefit>', 'The benefit you are claiming for. You may omit this if an OpenAI API key is configured')
.requiredOption('--amount <amount>', 'The amount of the claim')
.requiredOption('--merchant <merchant>', 'The name of the merchant')
.requiredOption('--category <category>', 'The category of the claim')
.option('--category <category>', 'The category of the claim. You may omit this if an OpenAI API key is configured.')
.requiredOption('--purchase-date <purchase-date>', 'The date of the purchase in YYYY-MM-DD format')
.requiredOption('--description <description>', 'The description of the claim')
.requiredOption('--receipt-path <receipt-path>', 'The path of the receipt. JPEG, PNG, PDF and HEIC files up to 10MB are accepted.')
.option('--access_token <access_token>', 'Access token used to authenticate with Forma')
.option('--openai-api-key <openai_token>', 'An optional OpenAI API key used to infer the benefit and category based on the merchant and description. If this is set, you may omit the `--benefit` and `--category` options. This can also be configured using the `OPENAI_API_KEY` environment variable.', process.env.OPENAI_API_KEY)
.action(actionRunner(async (opts) => {
const { benefit, category, openaiApiKey } = opts;
const accessToken = opts.accessToken ?? getAccessToken();
if (!accessToken) {
throw new Error("You aren't logged in to Forma. Please run `formanator login` first.");
}
const createClaimOptions = await claimParamsToCreateClaimOptions(opts, accessToken);
await createClaim(createClaimOptions);
if (benefit && category) {
const createClaimOptions = await claimParamsToCreateClaimOptions({ ...opts, benefit, category }, accessToken);
await createClaim(createClaimOptions);
}
else if (openaiApiKey) {
const benefitsWithCategories = await getBenefitsWithCategories(accessToken);
const { benefit, category } = await attemptToinferCategoryAndBenefit({
merchant: opts.merchant,
description: opts.description,
benefitsWithCategories,
openaiApiKey,
});
const createClaimOptions = await claimParamsToCreateClaimOptions({ ...opts, benefit, category }, accessToken);
await createClaim(createClaimOptions);
}
else {
throw new Error('You must either specify --benefit and --category, or an OpenAI API key.');
}
console.log(chalk.green('Claim submitted successfully ✅'));
}));
export default command;
10 changes: 10 additions & 0 deletions dist/forma.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export const getBenefits = async (accessToken) => {
remainingAmountCurrency,
}));
};
export const getBenefitsWithCategories = async (accessToken) => {
const benefits = await getBenefits(accessToken);
return await Promise.all(benefits.map(async (benefit) => {
const categories = await getCategoriesForBenefitName(accessToken, benefit.name);
return {
...benefit,
categories,
};
}));
};
export const createClaim = async (opts) => {
const { accessToken, amount, merchant, purchaseDate, description, receiptPath, benefitId, categoryId, subcategoryAlias, subcategoryValue, } = opts;
const formData = new FormData();
Expand Down
74 changes: 74 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"cli-table": "^0.3.11",
"commander": "^11.0.0",
"mime-types": "^2.1.35",
"openai": "^3.3.0",
"prompt-sync": "^4.2.0"
},
"repository": {
Expand Down

0 comments on commit 41f59b1

Please sign in to comment.