
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
contractor
Advanced tools
Contractor is a purpose-built, open-source library created in TypeScript. Designed with a primary focus on streamlining the usage of OpenAI's function calling and enabling incremental, type-safe streaming of JSON outputs, Contractor seamlessly integrates AI functionality into a wide array of applications.
Before getting started with Contractor, you should ensure that you meet the following requirements:
yarn add contractor
#or if you are using npm
npm install contractor
Creating a Contractor instance
const apiKey = process.env.OPENAI_API_KEY;
const logger = prefixedLogger('typedStreamingObjectWithAuditor'); // optional..
if (!apiKey) {
throw new Error('OPENAI_API_KEY env var not provided');
}
const configuration = new Configuration({
apiKey: apiKey, // your openai key..
});
const openaiClient = new OpenAIApi(configuration);
const client = new OpenAIClient(openaiClient);
const auditor: IAuditor<{ userId: string }> = {
/**
* @param record type AuditRecord<MetaData> = {
* resultRaw: CreateChatCompletionResponse | undefined;
* result: { data: { content: any } } | { error: { message: string, details: any, receivedMessage?: string } };
* request: CreateChatCompletionRequest;
* requestType: string;
* requestSig: string;
* metaData?: MetaData;
* }
*/
auditRequest: record => {
// write records of audit entries to db, use MetaData attribute to pass customerIds
// or other identifiers to associate record with business use
logger.info(`auditor record [${JSON.stringify(record)}]`)
}
};
const contractor = new Contractor(
client, // instance of OpenAIClient
auditor, // OPTIONAL - count token usage and log responses
800, // OPTIONAL (defaults to 8000)- max tokens per single request (request + response)
'<|+|>', // OPTIONAL (defaults to |{-*-}|) - seperator to use when using streaming
logger, // OPTIONAL - intercept interaly generated log entries
);
Using Contractor
// stream response
function streamingFunction(systemMessage: string,
messages: RequestMessageFormat[],
model: GPTModelsAlias, // gpt3/gpt4
functions: [ChatCompletionFunctionsWithTypes<T1, N1>], // array of functions to expose to GPT
transformObjectStream: (streamingObject: Result<T1, N1>) => Promise<OUT>, // handle resopnses from AI, make DB calls and pass on result downstream
responseSize?: number, // limit response size
logMetaData?: MetaData,
requestOverrides?: Partial<CreateChatCompletionRequest>, // override OpenAI arguments like top_p etc..
maxTokens?: number // limit total token usage for single call
): Promise<NodeJS.ReadableStream | undefined>;
// perform single function sync call (result returned via Promise)
function singleFunction<T>(systemMessage: string,
messages: RequestMessageFormat[],
model: GPTModelsAlias = 'gpt3', // gpt3/gpt4
gptFunction: {
name: string, // name of function
description: string; // helpful description
parameters: JSONSchemaType<T>; // JSPN schema to pass and validate against
},
logMetaData?: MetaData,
requestOverrides?: Partial<CreateChatCompletionRequest>,
responseSize: number = 2000,
maxTokens: number = this.maxTokensPerRequest,
): Promise<T | undefined>;
Simple agent (see full example)
const MathMultiplyOperation: JSONSchemaType<{ firstNumber: number, secondNumber: number }> = {
type: "object",
properties: {
firstNumber: {type: "number", description: "first number"},
secondNumber: {type: "number", description: "second number"},
},
required: ["firstNumber", "secondNumber"],
};
const MathAddOperation: JSONSchemaType<{ firstNumber: number, secondNumber: number }> = {
type: "object",
properties: {
firstNumber: {type: "number", description: "first number"},
secondNumber: {type: "number", description: "second number"},
},
required: ["firstNumber", "secondNumber"],
};
const FinalResultOperation: JSONSchemaType<{ finalResult: number }> = {
type: "object",
properties: {
finalResult: {type: "number", description: "first number"},
},
required: ["finalResult"],
};
contractor.streamingFunction('you are a calculator agent',
[{
role: 'user', content: `your goal is: ${JSON.stringify(currentGoal)}
operations performed so far: ${operationsPerformed.map(_ => JSON.stringify(_)).join('\n')}`
}],
'gpt3',
[
{
name: 'math_add_operation',
description: 'add two numbers',
parameters: MathAddOperation
},
{
name: 'math_multiply_operation',
description: 'multiply two numbers',
parameters: MathMultiplyOperation
},
{
name: 'final_result_operation',
description: 'return final math result',
parameters: FinalResultOperation
},
],
async streamingObject => {
if (streamingObject.name === 'math_add_operation') {
const opStack = [...operationsPerformed,
{calculated: `${streamingObject.value.firstNumber}+${streamingObject.value.secondNumber}=${streamingObject.value.firstNumber + streamingObject.value.secondNumber}`}];
performSingleOperation(currentGoal, opStack)
.then(resolve, reject);
return {
"performed": streamingObject.value,
"stack": opStack
}
} else if (streamingObject.name === 'math_multiply_operation') {
const opStack = [...operationsPerformed,
{calculated: `${streamingObject.value.firstNumber}*${streamingObject.value.secondNumber}=${streamingObject.value.firstNumber * streamingObject.value.secondNumber}`}];
performSingleOperation(currentGoal, opStack)
.then(resolve, reject);
return {
"performed": streamingObject.value,
"stack": opStack
}
} else if (streamingObject.name === 'final_result_operation')
resolve(`final result: ${JSON.stringify(streamingObject.value.finalResult)}`)
return {
"performed": streamingObject.value,
"stack": operationsPerformed,
"finalResult": streamingObject.value.finalResult
}
})
Incremental partial streaming (see full example)
const StoryOutput: JSONSchemaType<{ title: string, lines: string[] }> = {
type: "object",
properties: {
title: {type: "string"},
lines: {type: "array", description: "story line", items: {"type": 'string'}},
},
required: ['title', 'lines'],
};
contractor.streamingFunction('you are a story teller',
[{
role: 'user',
content: 'tell me a short story (50 lines) about an AI agent that went rogue, use available functions to answer'
}],
'gpt3',
[
{
name: 'print_output',
description: 'print content to user',
parameters: StoryOutput,
partialStreamPath: ['lines'],
},
],
async streamingObject => {
return `story so far: ${streamingObject.value.title}\n${streamingObject.value.lines.join('\n')}`
})
Using with Express server
// your express app..
const expressApp = express();
const oaiConf = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openaiClient = new OpenAIApi(oaiConf);
const client = new OpenAIClient(openaiClient);
const contractor = new Contractor(client);
const AnswerSchema: JSONSchemaType<{ richMarkdownAnswer: string }> = {
type: "object",
properties: {
richMarkdownAnswer: {type: "string", description: "richly markdown formatted answer"},
},
required: ["richMarkdownAnswer"],
};
expressApp.post('/ask-question-sync', async (req: Request, res: Response) => {
const {question} = req.body;
const value = await contractor.singleFunction("you are a calculator agent",
[{role: 'user', content: 'what is the sum of 2+2?'}],
'gpt3',
{
name: 'answer',
description: 'return answer',
parameters: AnswerSchema
}
);
res.json({result: value?.richMarkdownAnswer ?? 'no answer :('})
})
expressApp.post('/ask-question-async', async (req: Request, res: Response) => {
const {question} = req.body;
contractor.streamingFunction(
'you are a story teller', // system prompt
[{
role: 'user',
content: 'tell me a short story (50 lines) about an AI agent that went rogue, use available functions to answer'
}], // messages
'gpt3', // or gpt4
[
{
name: 'print_output',
description: 'print content to user',
parameters: StoryOutput,
// this is the important bit, the path inside `{ title: string, lines: string[] }`
// that should be streamed as the array parts come..
partialStreamPath: ['lines'],
},
], // array of functions to pass to ai, in the
async streamingObject => {
return `story so far: ${streamingObject.value.title}\n${streamingObject.value.lines.join('\n')}`
}
).then(stream => {
stream && stream.pipe(res);
});
})
For more examples see the example directory.
Some additional plug-and-play stuff you will need to get stuff done, like tests or your own use cases
StreamListenerTransformPlace an instance of this class in a stream to intercept chunks
import {pipeline} from "stream";
const stream = ...;
const listener = new StreamListenerTransform((x) => console.log("look what I got!", x));
pipeline(stream,
listener,
downstream,...)
SimpleStreamTransformPlace an instance of this class to manipulate chunks
import {pipeline} from "stream";
const stream = ...;
// transform here will transform incoming string chunks by trimming them
const tranform = new SimpleStreamTransform((input: string) => input.trim());
pipeline(stream,
tranform,
downstream,...)
StreamMITMTransformPlace an instance of this class to intercept string stream of stringified json, call a transform function (passed via constructor) and stringify back the result "down pipe".
import {pipeline} from "stream";
const stream = ...;
// transform here will transform incoming string chunks by trimming them
const tranform = new StreamMITMTransform((input: {text: string}) => input.text.trim());
pipeline(stream,
tranform,
downstream,...)
gptUtils - useful stuffimport {gptUtils} from "contractor";
// truncate some string to certain size of tokens
const input = "Alfalfa sprouts bananas chili roasted brussel sprouts fig arugula cashew salad dill main course chili pepper cashew creamiest edamame chocolate.";
const model = "gpt3" // or "gpt4"
const maxTokenSize = 10;
const truncatedString = gptUtils.truncateInput(input, model, maxTokenSize);
console.log(`we truncated input [${input}] to size [${maxTokenSize}] and `)
const tokenCount = gptUtils.countTokens(input, model);
console.log(`for string [${input}] we counted [${tokenCount}] tokens!`);
yarn test
cd contractor
yarn link
cd <your project>
yarn link contractor
We appreciate any contribution to Contractor, and thank you for your interest in improving this open-source project! Here's how you can contribute:
Fork the Repository: Start by forking the Contractor repository to your own GitHub account.
Clone the Repository: Clone the forked repository to your local machine and create a new branch for your feature or fix.
git clone https://github.com/<your-username>/contractor
git checkout -b name-of-your-branch
Make Changes: Implement your new feature or bug fix, making sure to add or update any relevant tests.
Run the Tests: Ensure that all tests pass with your changes.
yarn test
git push origin name-of-your-branch
In your pull request, please provide a clear description of the changes you've made. The more information you can provide, the easier it will be for us to review and accept your contribution.
Before submitting a pull request, please ensure that your code follows the existing style in the codebase.
Thank you for considering a contribution to Contractor. We're looking forward to your pull request!
FAQs
OpenAI made easy
The npm package contractor receives a total of 110 weekly downloads. As such, contractor popularity was classified as not popular.
We found that contractor demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.