修改: app/api/bedrock.ts

删除:     app/api/bedrock/models.ts
	删除:     app/api/bedrock/utils.ts
	修改:     app/client/platforms/bedrock.ts
	删除:     app/components/chat-actions.tsx
	修改:     app/components/chat.tsx
	修改:     app/constant.ts
	删除:     app/icons/document.svg
This commit is contained in:
glay 2024-11-04 16:24:10 +08:00
parent dca4a0e48f
commit fc391168e9
8 changed files with 355 additions and 1158 deletions

View File

@ -4,18 +4,350 @@ import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth"; import { auth } from "./auth";
import { import {
BedrockRuntimeClient, BedrockRuntimeClient,
ConverseStreamCommand,
ConverseStreamCommandInput,
ConverseStreamOutput, ConverseStreamOutput,
ValidationException,
ModelStreamErrorException, ModelStreamErrorException,
ThrottlingException, type Message,
ServiceUnavailableException, type ContentBlock,
InternalServerException, type SystemContentBlock,
type Tool,
type ToolChoice,
type ToolResultContentBlock,
} from "@aws-sdk/client-bedrock-runtime"; } from "@aws-sdk/client-bedrock-runtime";
import { validateModelId } from "./bedrock/utils";
import { ConverseRequest, createConverseStreamCommand } from "./bedrock/models";
// Constants and Types
const ALLOWED_PATH = new Set(["converse"]); const ALLOWED_PATH = new Set(["converse"]);
export interface ConverseRequest {
modelId: string;
messages: {
role: "user" | "assistant" | "system";
content: string | ContentItem[];
}[];
inferenceConfig?: {
maxTokens?: number;
temperature?: number;
topP?: number;
stopSequences?: string[];
};
toolConfig?: {
tools: Tool[];
toolChoice?: ToolChoice;
};
}
interface ContentItem {
type: "text" | "image_url" | "document" | "tool_use" | "tool_result";
text?: string;
image_url?: {
url: string; // base64 data URL
};
document?: {
format: DocumentFormat;
name: string;
source: {
bytes: string; // base64
};
};
tool_use?: {
tool_use_id: string;
name: string;
input: any;
};
tool_result?: {
tool_use_id: string;
content: ToolResultItem[];
status: "success" | "error";
};
}
interface ToolResultItem {
type: "text" | "image" | "document" | "json";
text?: string;
image?: {
format: "png" | "jpeg" | "gif" | "webp";
source: {
bytes: string; // base64
};
};
document?: {
format: DocumentFormat;
name: string;
source: {
bytes: string; // base64
};
};
json?: any;
}
type DocumentFormat =
| "pdf"
| "csv"
| "doc"
| "docx"
| "xls"
| "xlsx"
| "html"
| "txt"
| "md";
// Validation Functions
function validateModelId(modelId: string): string | null {
if (
modelId.startsWith("meta.llama") &&
!modelId.includes("inference-profile")
) {
return "Llama models require an inference profile. Please use the full inference profile ARN.";
}
return null;
}
function validateDocumentSize(base64Data: string): boolean {
const sizeInBytes = (base64Data.length * 3) / 4;
const maxSize = 4.5 * 1024 * 1024;
if (sizeInBytes > maxSize) {
throw new Error("Document size exceeds 4.5 MB limit");
}
return true;
}
function validateImageSize(base64Data: string): boolean {
const sizeInBytes = (base64Data.length * 3) / 4;
const maxSize = 3.75 * 1024 * 1024;
if (sizeInBytes > maxSize) {
throw new Error("Image size exceeds 3.75 MB limit");
}
return true;
}
// Content Processing Functions
function convertContentToAWSBlock(item: ContentItem): ContentBlock | null {
if (item.type === "text" && item.text) {
return { text: item.text };
}
if (item.type === "image_url" && item.image_url?.url) {
const base64Match = item.image_url.url.match(
/^data:image\/([a-zA-Z]*);base64,([^"]*)/,
);
if (base64Match) {
const format = base64Match[1].toLowerCase();
if (["png", "jpeg", "gif", "webp"].includes(format)) {
validateImageSize(base64Match[2]);
return {
image: {
format: format as "png" | "jpeg" | "gif" | "webp",
source: {
bytes: Uint8Array.from(Buffer.from(base64Match[2], "base64")),
},
},
};
}
}
}
if (item.type === "document" && item.document) {
validateDocumentSize(item.document.source.bytes);
return {
document: {
format: item.document.format,
name: item.document.name,
source: {
bytes: Uint8Array.from(
Buffer.from(item.document.source.bytes, "base64"),
),
},
},
};
}
if (item.type === "tool_use" && item.tool_use) {
return {
toolUse: {
toolUseId: item.tool_use.tool_use_id,
name: item.tool_use.name,
input: item.tool_use.input,
},
};
}
if (item.type === "tool_result" && item.tool_result) {
const toolResultContent = item.tool_result.content
.map((resultItem) => {
if (resultItem.type === "text" && resultItem.text) {
return { text: resultItem.text } as ToolResultContentBlock;
}
if (resultItem.type === "image" && resultItem.image) {
return {
image: {
format: resultItem.image.format,
source: {
bytes: Uint8Array.from(
Buffer.from(resultItem.image.source.bytes, "base64"),
),
},
},
} as ToolResultContentBlock;
}
if (resultItem.type === "document" && resultItem.document) {
return {
document: {
format: resultItem.document.format,
name: resultItem.document.name,
source: {
bytes: Uint8Array.from(
Buffer.from(resultItem.document.source.bytes, "base64"),
),
},
},
} as ToolResultContentBlock;
}
if (resultItem.type === "json" && resultItem.json) {
return { json: resultItem.json } as ToolResultContentBlock;
}
return null;
})
.filter((content): content is ToolResultContentBlock => content !== null);
if (toolResultContent.length === 0) {
return null;
}
return {
toolResult: {
toolUseId: item.tool_result.tool_use_id,
content: toolResultContent,
status: item.tool_result.status,
},
};
}
return null;
}
function convertContentToAWS(content: string | ContentItem[]): ContentBlock[] {
if (typeof content === "string") {
return [{ text: content }];
}
const blocks = content
.map(convertContentToAWSBlock)
.filter((block): block is ContentBlock => block !== null);
return blocks.length > 0 ? blocks : [{ text: "" }];
}
function formatMessages(messages: ConverseRequest["messages"]): {
messages: Message[];
systemPrompt?: SystemContentBlock[];
} {
const systemMessages = messages.filter((msg) => msg.role === "system");
const nonSystemMessages = messages.filter((msg) => msg.role !== "system");
const systemPrompt =
systemMessages.length > 0
? systemMessages.map((msg) => {
if (typeof msg.content === "string") {
return { text: msg.content } as SystemContentBlock;
}
const blocks = convertContentToAWS(msg.content);
return blocks[0] as SystemContentBlock;
})
: undefined;
const formattedMessages = nonSystemMessages.reduce(
(acc: Message[], curr, idx) => {
if (idx > 0 && curr.role === nonSystemMessages[idx - 1].role) {
return acc;
}
const content = convertContentToAWS(curr.content);
if (content.length > 0) {
acc.push({
role: curr.role as "user" | "assistant",
content,
});
}
return acc;
},
[],
);
if (formattedMessages.length === 0 || formattedMessages[0].role !== "user") {
formattedMessages.unshift({
role: "user",
content: [{ text: "Hello" }],
});
}
if (formattedMessages[formattedMessages.length - 1].role !== "user") {
formattedMessages.push({
role: "user",
content: [{ text: "Continue" }],
});
}
return { messages: formattedMessages, systemPrompt };
}
function formatRequestBody(
request: ConverseRequest,
): ConverseStreamCommandInput {
const { messages, systemPrompt } = formatMessages(request.messages);
const input: ConverseStreamCommandInput = {
modelId: request.modelId,
messages,
...(systemPrompt && { system: systemPrompt }),
};
if (request.inferenceConfig) {
input.inferenceConfig = {
maxTokens: request.inferenceConfig.maxTokens,
temperature: request.inferenceConfig.temperature,
topP: request.inferenceConfig.topP,
stopSequences: request.inferenceConfig.stopSequences,
};
}
if (request.toolConfig) {
input.toolConfig = {
tools: request.toolConfig.tools,
toolChoice: request.toolConfig.toolChoice,
};
}
const logInput = {
...input,
messages: messages.map((msg) => ({
role: msg.role,
content: msg.content?.map((content) => {
if ("image" in content && content.image) {
return {
image: {
format: content.image.format,
source: { bytes: "[BINARY]" },
},
};
}
if ("document" in content && content.document) {
return {
document: { ...content.document, source: { bytes: "[BINARY]" } },
};
}
return content;
}),
})),
};
console.log(
"[Bedrock] Formatted request:",
JSON.stringify(logInput, null, 2),
);
return input;
}
// Main Request Handler
export async function handle( export async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
@ -102,14 +434,13 @@ async function handleConverseRequest(req: NextRequest) {
console.log("[Bedrock] Invoking model:", modelId); console.log("[Bedrock] Invoking model:", modelId);
const command = createConverseStreamCommand(body); const command = new ConverseStreamCommand(formatRequestBody(body));
const response = await client.send(command); const response = await client.send(command);
if (!response.stream) { if (!response.stream) {
throw new Error("No stream in response"); throw new Error("No stream in response");
} }
// Create a ReadableStream for the response
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
try { try {
@ -190,58 +521,17 @@ async function handleConverseRequest(req: NextRequest) {
} }
controller.close(); controller.close();
} catch (error) { } catch (error) {
if (error instanceof ValidationException) { const errorResponse = {
controller.enqueue( type: "error",
`data: ${JSON.stringify({ error:
type: "error", error instanceof Error ? error.constructor.name : "UnknownError",
error: "ValidationException", message: error instanceof Error ? error.message : "Unknown error",
message: error.message, ...(error instanceof ModelStreamErrorException && {
})}\n\n`, originalStatusCode: error.originalStatusCode,
); originalMessage: error.originalMessage,
} else if (error instanceof ModelStreamErrorException) { }),
controller.enqueue( };
`data: ${JSON.stringify({ controller.enqueue(`data: ${JSON.stringify(errorResponse)}\n\n`);
type: "error",
error: "ModelStreamErrorException",
message: error.message,
originalStatusCode: error.originalStatusCode,
originalMessage: error.originalMessage,
})}\n\n`,
);
} else if (error instanceof ThrottlingException) {
controller.enqueue(
`data: ${JSON.stringify({
type: "error",
error: "ThrottlingException",
message: error.message,
})}\n\n`,
);
} else if (error instanceof ServiceUnavailableException) {
controller.enqueue(
`data: ${JSON.stringify({
type: "error",
error: "ServiceUnavailableException",
message: error.message,
})}\n\n`,
);
} else if (error instanceof InternalServerException) {
controller.enqueue(
`data: ${JSON.stringify({
type: "error",
error: "InternalServerException",
message: error.message,
})}\n\n`,
);
} else {
controller.enqueue(
`data: ${JSON.stringify({
type: "error",
error: "UnknownError",
message:
error instanceof Error ? error.message : "Unknown error",
})}\n\n`,
);
}
controller.close(); controller.close();
} }
}, },

View File

@ -1,405 +0,0 @@
import {
ConverseStreamCommand,
type ConverseStreamCommandInput,
type Message,
type ContentBlock,
type SystemContentBlock,
type Tool,
type ToolChoice,
type ToolResultContentBlock,
} from "@aws-sdk/client-bedrock-runtime";
export interface ConverseRequest {
modelId: string;
messages: {
role: "user" | "assistant" | "system";
content: string | ContentItem[];
}[];
inferenceConfig?: {
maxTokens?: number;
temperature?: number;
topP?: number;
stopSequences?: string[];
};
toolConfig?: {
tools: Tool[];
toolChoice?: ToolChoice;
};
}
interface ContentItem {
type: "text" | "image_url" | "document" | "tool_use" | "tool_result";
text?: string;
image_url?: {
url: string; // base64 data URL
};
document?: {
format:
| "pdf"
| "csv"
| "doc"
| "docx"
| "xls"
| "xlsx"
| "html"
| "txt"
| "md";
name: string;
source: {
bytes: string; // base64
};
};
tool_use?: {
tool_use_id: string;
name: string;
input: any;
};
tool_result?: {
tool_use_id: string;
content: ToolResultItem[];
status: "success" | "error";
};
}
interface ToolResultItem {
type: "text" | "image" | "document" | "json";
text?: string;
image?: {
format: "png" | "jpeg" | "gif" | "webp";
source: {
bytes: string; // base64
};
};
document?: {
format:
| "pdf"
| "csv"
| "doc"
| "docx"
| "xls"
| "xlsx"
| "html"
| "txt"
| "md";
name: string;
source: {
bytes: string; // base64
};
};
json?: any;
}
function convertContentToAWSBlock(item: ContentItem): ContentBlock | null {
if (item.type === "text" && item.text) {
return { text: item.text };
}
if (item.type === "image_url" && item.image_url?.url) {
const base64Match = item.image_url.url.match(
/^data:image\/([a-zA-Z]*);base64,([^"]*)/,
);
if (base64Match) {
const format = base64Match[1].toLowerCase();
if (
format === "png" ||
format === "jpeg" ||
format === "gif" ||
format === "webp"
) {
const base64Data = base64Match[2];
return {
image: {
format: format as "png" | "jpeg" | "gif" | "webp",
source: {
bytes: Uint8Array.from(Buffer.from(base64Data, "base64")),
},
},
};
}
}
}
if (item.type === "document" && item.document) {
return {
document: {
format: item.document.format,
name: item.document.name,
source: {
bytes: Uint8Array.from(
Buffer.from(item.document.source.bytes, "base64"),
),
},
},
};
}
if (item.type === "tool_use" && item.tool_use) {
return {
toolUse: {
toolUseId: item.tool_use.tool_use_id,
name: item.tool_use.name,
input: item.tool_use.input,
},
};
}
if (item.type === "tool_result" && item.tool_result) {
const toolResultContent = item.tool_result.content
.map((resultItem) => {
if (resultItem.type === "text" && resultItem.text) {
return { text: resultItem.text } as ToolResultContentBlock;
}
if (resultItem.type === "image" && resultItem.image) {
return {
image: {
format: resultItem.image.format,
source: {
bytes: Uint8Array.from(
Buffer.from(resultItem.image.source.bytes, "base64"),
),
},
},
} as ToolResultContentBlock;
}
if (resultItem.type === "document" && resultItem.document) {
return {
document: {
format: resultItem.document.format,
name: resultItem.document.name,
source: {
bytes: Uint8Array.from(
Buffer.from(resultItem.document.source.bytes, "base64"),
),
},
},
} as ToolResultContentBlock;
}
if (resultItem.type === "json" && resultItem.json) {
return { json: resultItem.json } as ToolResultContentBlock;
}
return null;
})
.filter((content): content is ToolResultContentBlock => content !== null);
if (toolResultContent.length === 0) {
return null;
}
return {
toolResult: {
toolUseId: item.tool_result.tool_use_id,
content: toolResultContent,
status: item.tool_result.status,
},
};
}
return null;
}
function convertContentToAWS(content: string | ContentItem[]): ContentBlock[] {
if (typeof content === "string") {
return [{ text: content }];
}
// Filter out null blocks and ensure each content block is valid
const blocks = content
.map(convertContentToAWSBlock)
.filter((block): block is ContentBlock => block !== null);
// If no valid blocks, provide a default text block
if (blocks.length === 0) {
return [{ text: "" }];
}
return blocks;
}
function formatMessages(messages: ConverseRequest["messages"]): {
messages: Message[];
systemPrompt?: SystemContentBlock[];
} {
// Extract system messages
const systemMessages = messages.filter((msg) => msg.role === "system");
const nonSystemMessages = messages.filter((msg) => msg.role !== "system");
// Convert system messages to SystemContentBlock array
const systemPrompt =
systemMessages.length > 0
? systemMessages.map((msg) => {
if (typeof msg.content === "string") {
return { text: msg.content } as SystemContentBlock;
}
// For multimodal content, convert each content item
const blocks = convertContentToAWS(msg.content);
return blocks[0] as SystemContentBlock; // Take first block as system content
})
: undefined;
// Format remaining messages
const formattedMessages = nonSystemMessages.reduce(
(acc: Message[], curr, idx) => {
// Skip if same role as previous message
if (idx > 0 && curr.role === nonSystemMessages[idx - 1].role) {
return acc;
}
const content = convertContentToAWS(curr.content);
if (content.length > 0) {
acc.push({
role: curr.role as "user" | "assistant",
content,
});
}
return acc;
},
[],
);
// Ensure conversation starts with user
if (formattedMessages.length === 0 || formattedMessages[0].role !== "user") {
formattedMessages.unshift({
role: "user",
content: [{ text: "Hello" }],
});
}
// Ensure conversation ends with user
if (formattedMessages[formattedMessages.length - 1].role !== "user") {
formattedMessages.push({
role: "user",
content: [{ text: "Continue" }],
});
}
return { messages: formattedMessages, systemPrompt };
}
export function formatRequestBody(
request: ConverseRequest,
): ConverseStreamCommandInput {
const { messages, systemPrompt } = formatMessages(request.messages);
const input: ConverseStreamCommandInput = {
modelId: request.modelId,
messages,
...(systemPrompt && { system: systemPrompt }),
};
if (request.inferenceConfig) {
input.inferenceConfig = {
maxTokens: request.inferenceConfig.maxTokens,
temperature: request.inferenceConfig.temperature,
topP: request.inferenceConfig.topP,
stopSequences: request.inferenceConfig.stopSequences,
};
}
if (request.toolConfig) {
input.toolConfig = {
tools: request.toolConfig.tools,
toolChoice: request.toolConfig.toolChoice,
};
}
// Create a clean version of the input for logging
const logInput = {
...input,
messages: messages.map((msg) => ({
role: msg.role,
content: msg.content?.map((content) => {
if ("image" in content && content.image) {
return {
image: {
format: content.image.format,
source: { bytes: "[BINARY]" },
},
};
}
if ("document" in content && content.document) {
return {
document: { ...content.document, source: { bytes: "[BINARY]" } },
};
}
return content;
}),
})),
};
console.log(
"[Bedrock] Formatted request:",
JSON.stringify(logInput, null, 2),
);
return input;
}
export function createConverseStreamCommand(request: ConverseRequest) {
const input = formatRequestBody(request);
return new ConverseStreamCommand(input);
}
export interface StreamResponse {
type:
| "messageStart"
| "contentBlockStart"
| "contentBlockDelta"
| "contentBlockStop"
| "messageStop"
| "metadata"
| "error";
role?: string;
index?: number;
start?: any;
delta?: any;
stopReason?: string;
additionalModelResponseFields?: any;
usage?: any;
metrics?: any;
trace?: any;
error?: string;
message?: string;
originalStatusCode?: number;
originalMessage?: string;
}
export function parseStreamResponse(chunk: any): StreamResponse | null {
if (chunk.messageStart) {
return { type: "messageStart", role: chunk.messageStart.role };
}
if (chunk.contentBlockStart) {
return {
type: "contentBlockStart",
index: chunk.contentBlockStart.contentBlockIndex,
start: chunk.contentBlockStart.start,
};
}
if (chunk.contentBlockDelta) {
return {
type: "contentBlockDelta",
index: chunk.contentBlockDelta.contentBlockIndex,
delta: chunk.contentBlockDelta.delta,
};
}
if (chunk.contentBlockStop) {
return {
type: "contentBlockStop",
index: chunk.contentBlockStop.contentBlockIndex,
};
}
if (chunk.messageStop) {
return {
type: "messageStop",
stopReason: chunk.messageStop.stopReason,
additionalModelResponseFields:
chunk.messageStop.additionalModelResponseFields,
};
}
if (chunk.metadata) {
return {
type: "metadata",
usage: chunk.metadata.usage,
metrics: chunk.metadata.metrics,
trace: chunk.metadata.trace,
};
}
return null;
}

View File

@ -1,400 +0,0 @@
import { MultimodalContent } from "../../client/api";
export interface Message {
role: string;
content: string | MultimodalContent[];
}
export interface ImageSource {
bytes: string; // base64 encoded image bytes
}
export interface DocumentSource {
bytes: string; // base64 encoded document bytes
media_type?: string; // MIME type of the document
}
export type DocumentFormat =
| "pdf"
| "csv"
| "doc"
| "docx"
| "xls"
| "xlsx"
| "html"
| "txt"
| "md";
export type ImageFormat = "png" | "jpeg" | "gif" | "webp";
export interface BedrockImageBlock {
type: "image";
image: {
format: ImageFormat;
source: {
bytes: string;
};
};
}
export interface BedrockDocumentBlock {
type: "document";
document: {
format: string;
name: string;
source: {
bytes: string;
media_type?: string;
};
};
}
export interface BedrockTextBlock {
type: "text";
text: string;
}
export interface BedrockToolCallBlock {
type: "tool_calls";
tool_calls: BedrockToolCall[];
}
export interface BedrockToolResultBlock {
type: "tool_result";
tool_result: BedrockToolResult;
}
export type BedrockContent =
| BedrockTextBlock
| BedrockImageBlock
| BedrockDocumentBlock
| BedrockToolCallBlock
| BedrockToolResultBlock;
export interface BedrockToolSpec {
type: string;
function: {
name: string;
description: string;
parameters: Record<string, any>;
};
}
export interface BedrockToolCall {
type: string;
function: {
name: string;
arguments: string;
};
}
export interface BedrockToolResult {
type: string;
output: string;
}
export interface ContentItem {
type: string;
text?: string;
image_url?: {
url: string;
};
document?: {
format: string;
name: string;
source: {
bytes: string;
media_type?: string;
};
};
tool_calls?: BedrockToolCall[];
tool_result?: BedrockToolResult;
}
export interface StreamEvent {
messageStart?: { role: string };
contentBlockStart?: { index: number };
contentBlockDelta?: {
delta: {
type?: string;
text?: string;
tool_calls?: BedrockToolCall[];
tool_result?: BedrockToolResult;
};
contentBlockIndex: number;
};
contentBlockStop?: { index: number };
messageStop?: { stopReason: string };
metadata?: {
usage: {
inputTokens: number;
outputTokens: number;
totalTokens: number;
};
metrics: {
latencyMs: number;
};
};
}
export interface ConverseRequest {
modelId: string;
messages: Message[];
inferenceConfig?: {
maxTokens?: number;
temperature?: number;
topP?: number;
stopSequences?: string[];
stream?: boolean;
};
system?: { text: string }[];
tools?: BedrockToolSpec[];
additionalModelRequestFields?: Record<string, any>;
additionalModelResponseFieldPaths?: string[];
}
export interface BedrockResponse {
content: BedrockContent[];
completion?: string;
stop_reason?: string;
usage?: {
input_tokens: number;
output_tokens: number;
total_tokens: number;
};
tool_calls?: BedrockToolCall[];
}
// Helper function to get the base model type from modelId
export function getModelType(modelId: string): string {
if (modelId.includes("inference-profile")) {
const match = modelId.match(/us\.(meta\.llama.+?)$/);
if (match) return match[1];
}
return modelId;
}
// Helper function to validate model ID
export function validateModelId(modelId: string): string | null {
// Check if model requires inference profile
if (
modelId.startsWith("meta.llama") &&
!modelId.includes("inference-profile")
) {
return "Llama models require an inference profile. Please use the full inference profile ARN.";
}
return null;
}
// Helper function to validate document name
export function validateDocumentName(name: string): boolean {
const validPattern = /^[a-zA-Z0-9\s\-\(\)\[\]]+$/;
const noMultipleSpaces = !/\s{2,}/.test(name);
return validPattern.test(name) && noMultipleSpaces;
}
// Helper function to validate document format
export function validateDocumentFormat(
format: string,
): format is DocumentFormat {
const validFormats: DocumentFormat[] = [
"pdf",
"csv",
"doc",
"docx",
"xls",
"xlsx",
"html",
"txt",
"md",
];
return validFormats.includes(format as DocumentFormat);
}
// Helper function to validate image size and dimensions
export function validateImageSize(base64Data: string): boolean {
// Check size (3.75 MB limit)
const sizeInBytes = (base64Data.length * 3) / 4; // Approximate size of decoded base64
const maxSize = 3.75 * 1024 * 1024; // 3.75 MB in bytes
if (sizeInBytes > maxSize) {
throw new Error("Image size exceeds 3.75 MB limit");
}
return true;
}
// Helper function to validate document size
export function validateDocumentSize(base64Data: string): boolean {
// Check size (4.5 MB limit)
const sizeInBytes = (base64Data.length * 3) / 4; // Approximate size of decoded base64
const maxSize = 4.5 * 1024 * 1024; // 4.5 MB in bytes
if (sizeInBytes > maxSize) {
throw new Error("Document size exceeds 4.5 MB limit");
}
return true;
}
// Helper function to process document content for Bedrock
export function processDocumentContent(content: any): BedrockDocumentBlock {
if (
!content?.document?.format ||
!content?.document?.name ||
!content?.document?.source?.bytes
) {
throw new Error("Invalid document content format");
}
const format = content.document.format.toLowerCase();
if (!validateDocumentFormat(format)) {
throw new Error(`Unsupported document format: ${format}`);
}
if (!validateDocumentName(content.document.name)) {
throw new Error(
`Invalid document name: ${content.document.name}. Only alphanumeric characters, single spaces, hyphens, parentheses, and square brackets are allowed.`,
);
}
// Validate document size
if (!validateDocumentSize(content.document.source.bytes)) {
throw new Error("Document size validation failed");
}
return {
type: "document",
document: {
format: format,
name: content.document.name,
source: {
bytes: content.document.source.bytes,
media_type: content.document.source.media_type,
},
},
};
}
// Helper function to process image content for Bedrock
export function processImageContent(content: any): BedrockImageBlock {
if (content.type === "image_url" && content.image_url?.url) {
const base64Match = content.image_url.url.match(
/^data:image\/([a-zA-Z]*);base64,([^"]*)$/,
);
if (base64Match) {
const format = base64Match[1].toLowerCase();
if (["png", "jpeg", "gif", "webp"].includes(format)) {
// Validate image size
if (!validateImageSize(base64Match[2])) {
throw new Error("Image size validation failed");
}
return {
type: "image",
image: {
format: format as ImageFormat,
source: {
bytes: base64Match[2],
},
},
};
}
}
}
throw new Error("Invalid image content format");
}
// Helper function to validate message content restrictions
export function validateMessageContent(message: Message): void {
if (Array.isArray(message.content)) {
// Count images and documents in user messages
if (message.role === "user") {
const imageCount = message.content.filter(
(item) => item.type === "image_url",
).length;
const documentCount = message.content.filter(
(item) => item.type === "document",
).length;
if (imageCount > 20) {
throw new Error("User messages can include up to 20 images");
}
if (documentCount > 5) {
throw new Error("User messages can include up to 5 documents");
}
} else if (
message.role === "assistant" &&
(message.content.some((item) => item.type === "image_url") ||
message.content.some((item) => item.type === "document"))
) {
throw new Error("Assistant messages cannot include images or documents");
}
}
}
// Helper function to ensure messages alternate between user and assistant
export function validateMessageOrder(messages: Message[]): Message[] {
const validatedMessages: Message[] = [];
let lastRole = "";
for (const message of messages) {
// Validate content restrictions for each message
validateMessageContent(message);
if (message.role === lastRole) {
// Skip duplicate roles to maintain alternation
continue;
}
validatedMessages.push(message);
lastRole = message.role;
}
return validatedMessages;
}
// Helper function to convert Bedrock response back to MultimodalContent format
export function convertBedrockResponseToMultimodal(
response: BedrockResponse,
): string | MultimodalContent[] {
if (response.completion) {
return response.completion;
}
if (!response.content) {
return "";
}
return response.content.map((block) => {
if (block.type === "text") {
return {
type: "text",
text: block.text,
};
} else if (block.type === "image") {
return {
type: "image_url",
image_url: {
url: `data:image/${block.image.format};base64,${block.image.source.bytes}`,
},
};
} else if (block.type === "document") {
return {
type: "document",
document: {
format: block.document.format,
name: block.document.name,
source: {
bytes: block.document.source.bytes,
media_type: block.document.source.media_type,
},
},
};
}
// Fallback to text content
return {
type: "text",
text: "",
};
});
}

View File

@ -239,7 +239,7 @@ export class BedrockApi implements LLMApi {
// Add error message as text content // Add error message as text content
content.push({ content.push({
type: "text", type: "text",
text: `Error processing image: ${e.message}`, text: `Error processing image: ${e}`,
}); });
} }
} }

View File

@ -1,188 +0,0 @@
import { ChatActions as Actions } from "./chat";
import DocumentIcon from "../icons/document.svg";
import LoadingButtonIcon from "../icons/loading.svg";
import { ServiceProvider } from "../constant";
import { useChatStore } from "../store";
import { showToast } from "./ui-lib";
import { MultimodalContent, MessageRole } from "../client/api";
import { ChatMessage } from "../store/chat";
export function ChatActions(props: Parameters<typeof Actions>[0]) {
const chatStore = useChatStore();
const currentProviderName =
chatStore.currentSession().mask.modelConfig?.providerName;
const isBedrockProvider = currentProviderName === ServiceProvider.Bedrock;
async function uploadDocument() {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".pdf,.csv,.doc,.docx,.xls,.xlsx,.html,.txt,.md";
fileInput.onchange = async (event: any) => {
const file = event.target.files[0];
if (!file) return;
props.setUploading(true);
try {
// Get file extension and MIME type
const format = file.name.split(".").pop()?.toLowerCase() || "";
const supportedFormats = [
"pdf",
"csv",
"doc",
"docx",
"xls",
"xlsx",
"html",
"txt",
"md",
];
if (!supportedFormats.includes(format)) {
throw new Error("Unsupported file format");
}
// Map file extensions to MIME types
const mimeTypes: { [key: string]: string } = {
pdf: "application/pdf",
csv: "text/csv",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
html: "text/html",
txt: "text/plain",
md: "text/markdown",
};
// Convert file to base64
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
if (!e.target?.result) return reject("Failed to read file");
// Get just the base64 data without the data URL prefix
const base64 = (e.target.result as string).split(",")[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
// Format file size
const size = file.size;
let sizeStr = "";
if (size < 1024) {
sizeStr = size + " B";
} else if (size < 1024 * 1024) {
sizeStr = (size / 1024).toFixed(2) + " KB";
} else {
sizeStr = (size / (1024 * 1024)).toFixed(2) + " MB";
}
// Create document content
const content: MultimodalContent[] = [
{
type: "text",
text: `Document: ${file.name} (${sizeStr})`,
},
{
type: "document",
document: {
format,
name: file.name,
source: {
bytes: base64,
media_type: mimeTypes[format] || `application/${format}`,
},
},
},
];
// Send content to Bedrock
const session = chatStore.currentSession();
const modelConfig = session.mask.modelConfig;
const api = await import("../client/api").then((m) =>
m.getClientApi(modelConfig.providerName),
);
// Create user message
const userMessage: ChatMessage = {
id: Date.now().toString(),
role: "user" as MessageRole,
content,
date: new Date().toLocaleString(),
isError: false,
};
// Create bot message
const botMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: "assistant" as MessageRole,
content: "",
date: new Date().toLocaleString(),
streaming: true,
isError: false,
};
// Add messages to session
chatStore.updateCurrentSession((session) => {
session.messages.push(userMessage, botMessage);
});
// Make request
api.llm.chat({
messages: [userMessage],
config: { ...modelConfig, stream: true },
onUpdate(message) {
botMessage.streaming = true;
if (message) {
botMessage.content = message;
}
chatStore.updateCurrentSession((session) => {
session.messages = session.messages.concat();
});
},
onFinish(message) {
botMessage.streaming = false;
if (message) {
botMessage.content = message;
chatStore.onNewMessage(botMessage);
}
},
onError(error) {
botMessage.content = error.message;
botMessage.streaming = false;
userMessage.isError = true;
botMessage.isError = true;
chatStore.updateCurrentSession((session) => {
session.messages = session.messages.concat();
});
console.error("[Chat] failed ", error);
},
});
} catch (error) {
console.error("Failed to upload document:", error);
showToast("Failed to upload document");
} finally {
props.setUploading(false);
}
};
fileInput.click();
}
return (
<div className="chat-input-actions">
{/* Original actions */}
<Actions {...props} />
{/* Document upload button (only for Bedrock) */}
{isBedrockProvider && (
<div className="chat-input-action">
<div className="icon" onClick={uploadDocument}>
{props.uploading ? <LoadingButtonIcon /> : <DocumentIcon />}
</div>
<div className="text">Upload Document</div>
</div>
)}
</div>
);
}

View File

@ -8,7 +8,7 @@ import React, {
Fragment, Fragment,
RefObject, RefObject,
} from "react"; } from "react";
import DocumentIcon from "../icons/document.svg";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
import RenameIcon from "../icons/rename.svg"; import RenameIcon from "../icons/rename.svg";
@ -548,91 +548,6 @@ export function ChatActions(props: {
); );
} }
}, [chatStore, currentModel, models]); }, [chatStore, currentModel, models]);
const isBedrockProvider = currentProviderName === ServiceProvider.Bedrock;
// ... (rest of the existing state and functions)
async function uploadDocument() {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".pdf,.csv,.doc,.docx,.xls,.xlsx,.html,.txt,.md";
fileInput.onchange = async (event: any) => {
const file = event.target.files[0];
if (!file) return;
props.setUploading(true);
try {
// Convert file to base64
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
if (!e.target?.result) return reject("Failed to read file");
const base64 = (e.target.result as string).split(",")[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
// Get file extension
const format = file.name.split(".").pop()?.toLowerCase() || "";
const supportedFormats = [
"pdf",
"csv",
"doc",
"docx",
"xls",
"xlsx",
"html",
"txt",
"md",
];
if (!supportedFormats.includes(format)) {
throw new Error("Unsupported file format");
}
// Format file size
const size = file.size;
let sizeStr = "";
if (size < 1024) {
sizeStr = size + " B";
} else if (size < 1024 * 1024) {
sizeStr = (size / 1024).toFixed(2) + " KB";
} else {
sizeStr = (size / (1024 * 1024)).toFixed(2) + " MB";
}
// Create document content with only filename and size
const documentContent = {
type: "document",
document: {
format,
name: file.name,
size: sizeStr,
source: {
bytes: base64,
},
},
};
// Submit the document content as a JSON string but only display filename and size
const displayContent = `Document: ${file.name} (${sizeStr})`;
chatStore.onUserInput(displayContent);
// Store the actual document content separately if needed
// chatStore.updateCurrentSession((session) => {
// session.lastDocument = documentContent;
// });
} catch (error) {
console.error("Failed to upload document:", error);
showToast("Failed to upload document");
} finally {
props.setUploading(false);
}
};
fileInput.click();
}
return ( return (
<div className={styles["chat-input-actions"]}> <div className={styles["chat-input-actions"]}>
@ -665,14 +580,6 @@ export function ChatActions(props: {
icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />} icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />}
/> />
)} )}
{/* Add document upload button for Bedrock */}
{isBedrockProvider && (
<ChatAction
onClick={uploadDocument}
text={Locale.Chat.InputActions.UploadDocument}
icon={props.uploading ? <LoadingButtonIcon /> : <DocumentIcon />}
/>
)}
<ChatAction <ChatAction
onClick={nextTheme} onClick={nextTheme}
text={Locale.Chat.InputActions.Theme[theme]} text={Locale.Chat.InputActions.Theme[theme]}

View File

@ -316,8 +316,8 @@ const bedrockModels = [
"anthropic.claude-3-opus-20240229-v1:0", "anthropic.claude-3-opus-20240229-v1:0",
"anthropic.claude-3-5-sonnet-20241022-v2:0", "anthropic.claude-3-5-sonnet-20241022-v2:0",
// Meta Llama Models // Meta Llama Models
"meta.llama3-2-11b-instruct-v1:0", "us.meta.llama3-2-11b-instruct-v1:0",
"meta.llama3-2-90b-instruct-v1:0", "us.meta.llama3-2-90b-instruct-v1:0",
//Mistral //Mistral
"mistral.mistral-large-2402-v1:0", "mistral.mistral-large-2402-v1:0",
"mistral.mistral-large-2407-v1:0", "mistral.mistral-large-2407-v1:0",
@ -529,7 +529,7 @@ export const DEFAULT_MODELS = [
id: "bedrock", id: "bedrock",
providerName: "Bedrock", providerName: "Bedrock",
providerType: "bedrock", providerType: "bedrock",
sorted: 12, sorted: 13,
}, },
})), })),
] as const; ] as const;

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="12" y1="18" x2="12" y2="12"/>
<line x1="9" y1="15" x2="15" y2="15"/>
</svg>

Before

Width:  |  Height:  |  Size: 399 B