Refactor to use newver v4 version of Vercel AI package

This commit is contained in:
eduardruzga 2024-12-09 17:26:33 +02:00
parent 6e61a4fb95
commit fcb61ba499
9 changed files with 441 additions and 608 deletions

View File

@ -112,13 +112,22 @@ export const ChatImpl = memo(
body: {
apiKeys,
},
sendExtraMessageFields: true,
onError: (error) => {
logger.error('Request failed\n\n', error);
toast.error(
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
);
},
onFinish: () => {
onFinish: (message, response) => {
const usage = response.usage;
if (usage) {
console.log('Token usage:', usage);
// You can now use the usage data as needed
}
logger.debug('Finished streaming');
},
initialMessages,

View File

@ -1,8 +1,8 @@
import React, { useState } from 'react';
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils';
import { createChatFromFolder } from '../../utils/folderImport';
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
import { createChatFromFolder } from '~/utils/folderImport';
interface ImportFolderButtonProps {
className?: string;
@ -17,12 +17,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
if (allFiles.length > MAX_FILES) {
toast.error(
`This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`
`This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
);
return;
}
const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
setIsLoading(true);
const loadingToast = toast.loading(`Importing ${folderName}...`);
try {

View File

@ -23,6 +23,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
disabled={disabled}
onClick={(event) => {
event.preventDefault();
if (!disabled) {
onClick?.(event);
}

View File

@ -8,17 +8,15 @@ export async function action(args: ActionFunctionArgs) {
return chatAction(args);
}
function parseCookies(cookieHeader: string) {
const cookies: any = {};
function parseCookies(cookieHeader: string): Record<string, string> {
const cookies: Record<string, string> = {};
// Split the cookie string by semicolons and spaces
const items = cookieHeader.split(';').map((cookie) => cookie.trim());
items.forEach((item) => {
const [name, ...rest] = item.split('=');
if (name && rest) {
// Decode the name and value, and join value parts in case it contains '='
const decodedName = decodeURIComponent(name.trim());
const decodedValue = decodeURIComponent(rest.join('=').trim());
cookies[decodedName] = decodedValue;
@ -35,16 +33,15 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}>();
const cookieHeader = request.headers.get('Cookie');
// Parse the cookie's value (returns an object or null if no cookie exists)
const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
const stream = new SwitchableStream();
try {
const options: StreamingOptions = {
toolChoice: 'none',
onFinish: async ({ text: content, finishReason }) => {
onFinish: async ({ text: content, finishReason, usage }) => {
console.log('usage', usage);
if (finishReason !== 'length') {
return stream.close();
}
@ -62,13 +59,12 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
return stream.switchSource(result.toAIStream());
return stream.switchSource(result.toDataStream());
},
};
const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
stream.switchSource(result.toAIStream());
stream.switchSource(result.toDataStream());
return new Response(stream.readable, {
status: 200,
@ -77,7 +73,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
},
});
} catch (error: any) {
console.log(error);
console.error(error);
if (error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', {

View File

@ -1,5 +1,6 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { StreamingTextResponse, parseStreamPart } from 'ai';
//import { StreamingTextResponse, parseStreamPart } from 'ai';
import { streamText } from '~/lib/.server/llm/stream-text';
import { stripIndents } from '~/utils/stripIndent';
import type { ProviderInfo } from '~/types/model';
@ -45,32 +46,32 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
`[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
stripIndents`
You are a professional prompt engineer specializing in crafting precise, effective prompts.
Your task is to enhance prompts by making them more specific, actionable, and effective.
Your task is to enhance prompts by making them more specific, actionable, and effective.
I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
For valid prompts:
- Make instructions explicit and unambiguous
- Add relevant context and constraints
- Remove redundant information
- Maintain the core intent
- Ensure the prompt is self-contained
- Use professional language
For valid prompts:
- Make instructions explicit and unambiguous
- Add relevant context and constraints
- Remove redundant information
- Maintain the core intent
- Ensure the prompt is self-contained
- Use professional language
For invalid or unclear prompts:
- Respond with a clear, professional guidance message
- Keep responses concise and actionable
- Maintain a helpful, constructive tone
- Focus on what the user should provide
- Use a standard template for consistency
For invalid or unclear prompts:
- Respond with clear, professional guidance
- Keep responses concise and actionable
- Maintain a helpful, constructive tone
- Focus on what the user should provide
- Use a standard template for consistency
IMPORTANT: Your response must ONLY contain the enhanced prompt text.
Do not include any explanations, metadata, or wrapper tags.
IMPORTANT: Your response must ONLY contain the enhanced prompt text.
Do not include any explanations, metadata, or wrapper tags.
<original_prompt>
${message}
</original_prompt>
`,
<original_prompt>
${message}
</original_prompt>
`,
},
],
context.cloudflare.env,
@ -85,7 +86,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
for (const line of lines) {
try {
const parsed = parseStreamPart(line);
const parsed = JSON.parse(line);
if (parsed.type === 'text') {
controller.enqueue(encoder.encode(parsed.value));
@ -100,7 +101,12 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
const transformedStream = result.toDataStream().pipeThrough(transformStream);
return new StreamingTextResponse(transformedStream);
return new Response(transformedStream, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
} catch (error: unknown) {
console.log(error);

View File

@ -29,10 +29,12 @@ export const isBinaryFile = async (file: File): Promise<boolean> => {
for (let i = 0; i < buffer.length; i++) {
const byte = buffer[i];
if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
return true;
}
}
return false;
};
@ -41,8 +43,11 @@ export const shouldIncludeFile = (path: string): boolean => {
};
const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json'));
if (!packageJsonFile) return null;
const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json'));
if (!packageJsonFile) {
return null;
}
try {
const content = await new Promise<string>((resolve, reject) => {
@ -59,29 +64,32 @@ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string
}
};
export const detectProjectType = async (files: File[]): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name));
export const detectProjectType = async (
files: File[],
): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name));
if (hasFile('package.json')) {
const packageJson = await readPackageJson(files);
const scripts = packageJson?.scripts || {};
// Check for preferred commands in priority order
const preferredCommands = ['dev', 'start', 'preview'];
const availableCommand = preferredCommands.find(cmd => scripts[cmd]);
const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
if (availableCommand) {
return {
type: 'Node.js',
setupCommand: `npm install && npm run ${availableCommand}`,
followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`
followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
};
}
return {
type: 'Node.js',
setupCommand: 'npm install',
followupMessage: 'Would you like me to inspect package.json to determine the available scripts for running this project?'
followupMessage:
'Would you like me to inspect package.json to determine the available scripts for running this project?',
};
}
@ -89,7 +97,7 @@ export const detectProjectType = async (files: File[]): Promise<{ type: string;
return {
type: 'Static',
setupCommand: 'npx --yes serve',
followupMessage: ''
followupMessage: '',
};
}

View File

@ -4,12 +4,13 @@ import { generateId, detectProjectType } from './fileUtils';
export const createChatFromFolder = async (
files: File[],
binaryFiles: string[],
folderName: string
folderName: string,
): Promise<Message[]> => {
const fileArtifacts = await Promise.all(
files.map(async (file) => {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const content = reader.result as string;
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
@ -26,31 +27,37 @@ ${content}
);
const project = await detectProjectType(files);
const setupCommand = project.setupCommand ? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>` : '';
const setupCommand = project.setupCommand
? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>`
: '';
const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : '';
const binaryFilesMessage = binaryFiles.length > 0
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
: '';
const binaryFilesMessage =
binaryFiles.length > 0
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
: '';
const assistantMessages: Message[] = [{
role: 'assistant',
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
const assistantMessages: Message[] = [
{
role: 'assistant',
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
<boltArtifact id="imported-files" title="Imported Files">
${fileArtifacts.join('\n\n')}
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
},{
role: 'assistant',
content: `
id: generateId(),
createdAt: new Date(),
},
{
role: 'assistant',
content: `
<boltArtifact id="imported-files" title="Imported Files">
${setupCommand}
</boltArtifact>${followupMessage}`,
id: generateId(),
createdAt: new Date(),
}];
id: generateId(),
createdAt: new Date(),
},
];
const userMessage: Message = {
role: 'user',
@ -59,5 +66,5 @@ ${setupCommand}
createdAt: new Date(),
};
return [ userMessage, ...assistantMessages ];
return [userMessage, ...assistantMessages];
};

View File

@ -69,7 +69,7 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"ai": "^3.4.33",
"ai": "^4.0.13",
"date-fns": "^3.6.0",
"diff": "^5.2.0",
"file-saver": "^2.0.5",

File diff suppressed because it is too large Load Diff