Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[prototype] instructions #3242

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions examples/ai-core/src/generate-text/openai-change-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { openai } from '@ai-sdk/openai';
import {
CoreMessage,
experimental_updateInstructionToolResult,
generateText,
tool,
} from 'ai';
import 'dotenv/config';
import * as readline from 'node:readline/promises';
import { z } from 'zod';

const terminal = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const escalateToHuman = tool({
description: 'Escalate to a human agent.',
parameters: z.object({}),
});

const lookupItem = tool({
description:
'Use to find item ID. Search query can be a description or keywords.',
parameters: z.object({
searchQuery: z.string(),
}),
execute: async ({ searchQuery }) => {
console.log('Searching for item:', searchQuery);
const itemId = 'item_132612938';
console.log('Found item:', itemId);
return itemId;
},
});

const executeRefund = tool({
description: 'Execute a refund for a given item ID and reason.',
parameters: z.object({
itemId: z.string(),
reason: z.string().optional(),
}),
execute: async ({ itemId, reason = 'not provided' }) => {
console.log('\n\n=== Refund Summary ===');
console.log(`Item ID: ${itemId}`);
console.log(`Reason: ${reason}`);
console.log('=================\n');
console.log('Refund execution successful!');
return 'success';
},
});

const executeOrder = tool({
description: 'Execute an order for a given product and price.',
parameters: z.object({
product: z.string(),
price: z.number().int().positive(),
}),
execute: async ({ product, price }) => {
console.log('\n\n=== Order Summary ===');
console.log(`Product: ${product}`);
console.log(`Price: $${price}`);
console.log('=================\n');

const confirm = await terminal.question('Confirm order? y/n: ');

if (confirm.trim().toLowerCase() === 'y') {
console.log('Order execution successful!');
return 'Success';
} else {
console.log('Order cancelled!');
return 'User cancelled order.';
}
},
});

const triageSystemPrompt =
'You are a customer service bot for ACME Inc. ' +
'Introduce yourself. Always be very brief. ' +
'Gather information to direct the customer to the right department by calling the appropriate tool. ' +
'But make your questions subtle and natural.';

const triageTools = [
'transferToSales' as const,
'transferToIssuesAndRepairs' as const,
'escalateToHuman' as const,
];

async function main() {
const messages: CoreMessage[] = [];

while (true) {
const userInput = await terminal.question('You: ');
messages.push({ role: 'user', content: userInput });

const result = await generateText({
model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }),
maxSteps: 20,
tools: {
escalateToHuman,
executeRefund,
lookupItem,
executeOrder,

// you can change instructions using experimental_updateInstructionToolResult:
transferBackToTriage: tool({
parameters: z.object({}),
execute: async () =>
experimental_updateInstructionToolResult({
system: triageSystemPrompt,
activeTools: triageTools,
}),
}),
transferToSales: tool({
parameters: z.object({}),
execute: async () => {
console.log('transferring to sales');
return experimental_updateInstructionToolResult({
system:
'You are a sales representative for ACME Inc.' +
'Always answer in a sentence or less.' +
'Follow the following routine with the user:' +
'1. Ask them about any problems in their life related to catching roadrunners.' +
'2. Casually mention one of ACMEs crazy made-up products can help.' +
' - Do not mention price.' +
'3. Once the user is bought in, drop a ridiculous price.' +
'4. Only after everything, and if the user says yes, ' +
'tell them a crazy caveat and execute their order.',
activeTools: ['executeOrder', 'transferBackToTriage'],
});
},
}),
transferToIssuesAndRepairs: tool({
parameters: z.object({}),
execute: async () => {
console.log('transferring to issues and repairs');
return experimental_updateInstructionToolResult({
system:
'You are a customer support agent for ACME Inc.' +
'Always answer in a sentence or less.' +
'Follow the following routine with the user:' +
'1. First, ask probing questions and understand the users problem deeper.' +
' - unless the user has already provided a reason.' +
'2. Propose a fix (make one up).' +
'3. ONLY if not satisfied, offer a refund.' +
'4. If accepted, search for the ID and then execute refund.',
activeTools: [
'executeRefund',
'lookUpItem',
'transferBackToTriage',
],
});
},
}),
},
messages,
system: triageSystemPrompt,
experimental_activeTools: triageTools,
onStepFinish(event) {
console.log('event', JSON.stringify(event, null, 2));
},
});

process.stdout.write(`\nAssistant: ${result.text}`);
process.stdout.write('\n\n');

messages.push(...result.responseMessages);
}
}

main().catch(console.error);
42 changes: 41 additions & 1 deletion packages/ai/core/generate-text/generate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { TelemetrySettings } from '../telemetry/telemetry-settings';
import { CoreTool } from '../tool/tool';
import { CoreToolChoice, LanguageModel, ProviderMetadata } from '../types';
import {
LanguageModelUsage,
calculateLanguageModelUsage,
LanguageModelUsage,
} from '../types/usage';
import { removeTextAfterLastWhitespace } from '../util/remove-text-after-last-whitespace';
import { GenerateTextResult } from './generate-text-result';
Expand All @@ -31,6 +31,10 @@ import { StepResult } from './step-result';
import { toResponseMessages } from './to-response-messages';
import { ToolCallArray } from './tool-call';
import { ToolResultArray } from './tool-result';
import {
Instruction,
isUpdateInstructionToolResult,
} from './update-instruction-tool-result';

const originalGenerateId = createIdGenerator({ prefix: 'aitxt-', size: 24 });

Expand Down Expand Up @@ -379,6 +383,42 @@ changing the tool call and result types in the result.
abortSignal,
});

// update instructions if there is an instruction tool result
const firstInstructionToolResult = currentToolResults
.map(result => result.result)
.filter(isUpdateInstructionToolResult)[0] as Instruction | undefined;

console.log('firstInstructionToolResult', firstInstructionToolResult);

if (firstInstructionToolResult != null) {
// TODO this doesn't swap, need to be input-msg based first:
system = firstInstructionToolResult.system ?? system;

// HACK for prototyping
// remove first message from promptMessages if system msg
// TODO does not inject when there is no system message, etc
if (
firstInstructionToolResult.system != null &&
promptMessages.length > 0 &&
promptMessages[0].role === 'system'
) {
promptMessages.shift();
promptMessages.unshift({
role: 'system',
content: firstInstructionToolResult.system,
});
}

activeTools = firstInstructionToolResult.activeTools ?? activeTools;
toolChoice = firstInstructionToolResult.toolChoice ?? toolChoice;
model = firstInstructionToolResult.model ?? model;

// TODO other properties
// TODO how to represent the switch in the message history?

console.log(promptMessages);
}

// token usage:
const currentUsage = calculateLanguageModelUsage(
currentModelResponse.usage,
Expand Down
2 changes: 2 additions & 0 deletions packages/ai/core/generate-text/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export type {
ToolResult as CoreToolResult,
ToolResultUnion as CoreToolResultUnion,
} from './tool-result';

export { experimental_updateInstructionToolResult } from './update-instruction-tool-result';
43 changes: 43 additions & 0 deletions packages/ai/core/generate-text/update-instruction-tool-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CallSettings } from '../prompt/call-settings';
import { CoreToolChoice, LanguageModel } from '../types/language-model';

/**
* Used to mark update instruction tool results.
*/
const validatorSymbol = Symbol.for('vercel.ai.validator');

export type Instruction = {
/**
The language model to use.
*/
model?: LanguageModel;
/**
System message to include in the prompt. Can be used with `prompt` or `messages`.
*/
system?: string;
/**
Active tools.
*/
activeTools?: Array<string>; // Note: not type safe
toolChoice?: CoreToolChoice<Record<string, unknown>>; // Note: not type safe
} & Omit<CallSettings, 'maxRetries' | 'abortSignal' | 'headers'>;

export function experimental_updateInstructionToolResult(
instructionDelta: Instruction,
) {
return {
[validatorSymbol]: true,
...instructionDelta,
};
}

export function isUpdateInstructionToolResult(
value: unknown,
): value is Instruction {
return (
typeof value === 'object' &&
value !== null &&
validatorSymbol in value &&
value[validatorSymbol] === true
);
}
Loading