mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
feat: add @automaker/utils package
- Extract error handling utilities (isAbortError, classifyError, etc.) - Extract conversation utilities (formatHistoryAsText, etc.) - Extract image handling utilities (readImageAsBase64, etc.) - Extract prompt building utilities (buildPromptWithImages) - Extract logger utilities (createLogger, setLogLevel) - Extract file system utilities (mkdirSafe, existsSafe) All utilities now use @automaker/types for type imports. Provides shared utility functions for both server and UI. Dependencies: @automaker/types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
21
libs/utils/package.json
Normal file
21
libs/utils/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@automaker/utils",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Shared utility functions for AutoMaker",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc --watch"
|
||||||
|
},
|
||||||
|
"keywords": ["automaker", "utils"],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@automaker/types": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
97
libs/utils/src/conversation-utils.ts
Normal file
97
libs/utils/src/conversation-utils.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Conversation history utilities for processing message history
|
||||||
|
*
|
||||||
|
* Provides standardized conversation history handling:
|
||||||
|
* - Extract text from content (string or array format)
|
||||||
|
* - Normalize content blocks to array format
|
||||||
|
* - Format history as plain text for CLI-based providers
|
||||||
|
* - Convert history to Claude SDK message format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConversationMessage } from '@automaker/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract plain text from message content (handles both string and array formats)
|
||||||
|
*
|
||||||
|
* @param content - Message content (string or array of content blocks)
|
||||||
|
* @returns Extracted text content
|
||||||
|
*/
|
||||||
|
export function extractTextFromContent(
|
||||||
|
content: string | Array<{ type: string; text?: string; source?: object }>
|
||||||
|
): string {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text blocks only
|
||||||
|
return content
|
||||||
|
.filter((block) => block.type === "text")
|
||||||
|
.map((block) => block.text || "")
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize message content to array format
|
||||||
|
*
|
||||||
|
* @param content - Message content (string or array)
|
||||||
|
* @returns Content as array of blocks
|
||||||
|
*/
|
||||||
|
export function normalizeContentBlocks(
|
||||||
|
content: string | Array<{ type: string; text?: string; source?: object }>
|
||||||
|
): Array<{ type: string; text?: string; source?: object }> {
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return [{ type: "text", text: content }];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format conversation history as plain text for CLI-based providers
|
||||||
|
*
|
||||||
|
* @param history - Array of conversation messages
|
||||||
|
* @returns Formatted text with role labels
|
||||||
|
*/
|
||||||
|
export function formatHistoryAsText(history: ConversationMessage[]): string {
|
||||||
|
if (history.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let historyText = "Previous conversation:\n\n";
|
||||||
|
|
||||||
|
for (const msg of history) {
|
||||||
|
const contentText = extractTextFromContent(msg.content);
|
||||||
|
const role = msg.role === "user" ? "User" : "Assistant";
|
||||||
|
historyText += `${role}: ${contentText}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
historyText += "---\n\n";
|
||||||
|
return historyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert conversation history to Claude SDK message format
|
||||||
|
*
|
||||||
|
* @param history - Array of conversation messages
|
||||||
|
* @returns Array of Claude SDK formatted messages
|
||||||
|
*/
|
||||||
|
export function convertHistoryToMessages(
|
||||||
|
history: ConversationMessage[]
|
||||||
|
): Array<{
|
||||||
|
type: "user" | "assistant";
|
||||||
|
session_id: string;
|
||||||
|
message: {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: Array<{ type: string; text?: string; source?: object }>;
|
||||||
|
};
|
||||||
|
parent_tool_use_id: null;
|
||||||
|
}> {
|
||||||
|
return history.map((historyMsg) => ({
|
||||||
|
type: historyMsg.role,
|
||||||
|
session_id: "",
|
||||||
|
message: {
|
||||||
|
role: historyMsg.role,
|
||||||
|
content: normalizeContentBlocks(historyMsg.content),
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
110
libs/utils/src/error-handler.ts
Normal file
110
libs/utils/src/error-handler.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Error handling utilities for standardized error classification
|
||||||
|
*
|
||||||
|
* Provides utilities for:
|
||||||
|
* - Detecting abort/cancellation errors
|
||||||
|
* - Detecting authentication errors
|
||||||
|
* - Classifying errors by type
|
||||||
|
* - Generating user-friendly error messages
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ErrorType, ErrorInfo } from '@automaker/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is an abort/cancellation error
|
||||||
|
*
|
||||||
|
* @param error - The error to check
|
||||||
|
* @returns True if the error is an abort error
|
||||||
|
*/
|
||||||
|
export function isAbortError(error: unknown): boolean {
|
||||||
|
return (
|
||||||
|
error instanceof Error &&
|
||||||
|
(error.name === "AbortError" || error.message.includes("abort"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is a user-initiated cancellation
|
||||||
|
*
|
||||||
|
* @param errorMessage - The error message to check
|
||||||
|
* @returns True if the error is a user-initiated cancellation
|
||||||
|
*/
|
||||||
|
export function isCancellationError(errorMessage: string): boolean {
|
||||||
|
const lowerMessage = errorMessage.toLowerCase();
|
||||||
|
return (
|
||||||
|
lowerMessage.includes("cancelled") ||
|
||||||
|
lowerMessage.includes("canceled") ||
|
||||||
|
lowerMessage.includes("stopped") ||
|
||||||
|
lowerMessage.includes("aborted")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is an authentication/API key error
|
||||||
|
*
|
||||||
|
* @param errorMessage - The error message to check
|
||||||
|
* @returns True if the error is authentication-related
|
||||||
|
*/
|
||||||
|
export function isAuthenticationError(errorMessage: string): boolean {
|
||||||
|
return (
|
||||||
|
errorMessage.includes("Authentication failed") ||
|
||||||
|
errorMessage.includes("Invalid API key") ||
|
||||||
|
errorMessage.includes("authentication_failed") ||
|
||||||
|
errorMessage.includes("Fix external API key")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify an error into a specific type
|
||||||
|
*
|
||||||
|
* @param error - The error to classify
|
||||||
|
* @returns Classified error information
|
||||||
|
*/
|
||||||
|
export function classifyError(error: unknown): ErrorInfo {
|
||||||
|
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
||||||
|
const isAbort = isAbortError(error);
|
||||||
|
const isAuth = isAuthenticationError(message);
|
||||||
|
const isCancellation = isCancellationError(message);
|
||||||
|
|
||||||
|
let type: ErrorType;
|
||||||
|
if (isAuth) {
|
||||||
|
type = "authentication";
|
||||||
|
} else if (isAbort) {
|
||||||
|
type = "abort";
|
||||||
|
} else if (isCancellation) {
|
||||||
|
type = "cancellation";
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
type = "execution";
|
||||||
|
} else {
|
||||||
|
type = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
isAbort,
|
||||||
|
isAuth,
|
||||||
|
isCancellation,
|
||||||
|
originalError: error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a user-friendly error message
|
||||||
|
*
|
||||||
|
* @param error - The error to convert
|
||||||
|
* @returns User-friendly error message
|
||||||
|
*/
|
||||||
|
export function getUserFriendlyErrorMessage(error: unknown): string {
|
||||||
|
const info = classifyError(error);
|
||||||
|
|
||||||
|
if (info.isAbort) {
|
||||||
|
return "Operation was cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.isAuth) {
|
||||||
|
return "Authentication failed. Please check your API key.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.message;
|
||||||
|
}
|
||||||
67
libs/utils/src/fs-utils.ts
Normal file
67
libs/utils/src/fs-utils.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* File system utilities that handle symlinks safely
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a directory, handling symlinks safely to avoid ELOOP errors.
|
||||||
|
* If the path already exists as a directory or symlink, returns success.
|
||||||
|
*/
|
||||||
|
export async function mkdirSafe(dirPath: string): Promise<void> {
|
||||||
|
const resolvedPath = path.resolve(dirPath);
|
||||||
|
|
||||||
|
// Check if path already exists using lstat (doesn't follow symlinks)
|
||||||
|
try {
|
||||||
|
const stats = await fs.lstat(resolvedPath);
|
||||||
|
// Path exists - if it's a directory or symlink, consider it success
|
||||||
|
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// It's a file - can't create directory
|
||||||
|
throw new Error(`Path exists and is not a directory: ${resolvedPath}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
// ENOENT means path doesn't exist - we should create it
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
// Some other error (could be ELOOP in parent path)
|
||||||
|
// If it's ELOOP, the path involves symlinks - don't try to create
|
||||||
|
if (error.code === "ELOOP") {
|
||||||
|
console.warn(`[fs-utils] Symlink loop detected at ${resolvedPath}, skipping mkdir`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path doesn't exist, create it
|
||||||
|
try {
|
||||||
|
await fs.mkdir(resolvedPath, { recursive: true });
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle race conditions and symlink issues
|
||||||
|
if (error.code === "EEXIST" || error.code === "ELOOP") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a path exists, handling symlinks safely.
|
||||||
|
* Returns true if the path exists as a file, directory, or symlink.
|
||||||
|
*/
|
||||||
|
export async function existsSafe(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.lstat(filePath);
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ELOOP or other errors - path exists but is problematic
|
||||||
|
if (error.code === "ELOOP") {
|
||||||
|
return true; // Symlink exists, even if looping
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
libs/utils/src/image-handler.ts
Normal file
114
libs/utils/src/image-handler.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Image handling utilities for processing image files
|
||||||
|
*
|
||||||
|
* Provides utilities for:
|
||||||
|
* - MIME type detection based on file extensions
|
||||||
|
* - Base64 encoding of image files
|
||||||
|
* - Content block generation for Claude SDK format
|
||||||
|
* - Path resolution (relative/absolute)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import type { ImageData, ImageContentBlock } from '@automaker/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIME type mapping for image file extensions
|
||||||
|
*/
|
||||||
|
const IMAGE_MIME_TYPES: Record<string, string> = {
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".png": "image/png",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MIME type for an image file based on extension
|
||||||
|
*
|
||||||
|
* @param imagePath - Path to the image file
|
||||||
|
* @returns MIME type string (defaults to "image/png" for unknown extensions)
|
||||||
|
*/
|
||||||
|
export function getMimeTypeForImage(imagePath: string): string {
|
||||||
|
const ext = path.extname(imagePath).toLowerCase();
|
||||||
|
return IMAGE_MIME_TYPES[ext] || "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read an image file and convert to base64 with metadata
|
||||||
|
*
|
||||||
|
* @param imagePath - Path to the image file
|
||||||
|
* @returns Promise resolving to image data with base64 encoding
|
||||||
|
* @throws Error if file cannot be read
|
||||||
|
*/
|
||||||
|
export async function readImageAsBase64(imagePath: string): Promise<ImageData> {
|
||||||
|
const imageBuffer = await fs.readFile(imagePath);
|
||||||
|
const base64Data = imageBuffer.toString("base64");
|
||||||
|
const mimeType = getMimeTypeForImage(imagePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
base64: base64Data,
|
||||||
|
mimeType,
|
||||||
|
filename: path.basename(imagePath),
|
||||||
|
originalPath: imagePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert image paths to content blocks (Claude SDK format)
|
||||||
|
* Handles both relative and absolute paths
|
||||||
|
*
|
||||||
|
* @param imagePaths - Array of image file paths
|
||||||
|
* @param workDir - Optional working directory for resolving relative paths
|
||||||
|
* @returns Promise resolving to array of image content blocks
|
||||||
|
*/
|
||||||
|
export async function convertImagesToContentBlocks(
|
||||||
|
imagePaths: string[],
|
||||||
|
workDir?: string
|
||||||
|
): Promise<ImageContentBlock[]> {
|
||||||
|
const blocks: ImageContentBlock[] = [];
|
||||||
|
|
||||||
|
for (const imagePath of imagePaths) {
|
||||||
|
try {
|
||||||
|
// Resolve to absolute path if needed
|
||||||
|
const absolutePath = workDir && !path.isAbsolute(imagePath)
|
||||||
|
? path.join(workDir, imagePath)
|
||||||
|
: imagePath;
|
||||||
|
|
||||||
|
const imageData = await readImageAsBase64(absolutePath);
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: imageData.mimeType,
|
||||||
|
data: imageData.base64,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ImageHandler] Failed to load image ${imagePath}:`, error);
|
||||||
|
// Continue processing other images
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a list of image paths for text prompts
|
||||||
|
* Formats image paths as a bulleted list for inclusion in text prompts
|
||||||
|
*
|
||||||
|
* @param imagePaths - Array of image file paths
|
||||||
|
* @returns Formatted string with image paths, or empty string if no images
|
||||||
|
*/
|
||||||
|
export function formatImagePathsForPrompt(imagePaths: string[]): string {
|
||||||
|
if (imagePaths.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = "\n\nAttached images:\n";
|
||||||
|
for (const imagePath of imagePaths) {
|
||||||
|
text += `- ${imagePath}\n`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
50
libs/utils/src/index.ts
Normal file
50
libs/utils/src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @automaker/utils
|
||||||
|
* Shared utility functions for AutoMaker
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
export {
|
||||||
|
isAbortError,
|
||||||
|
isCancellationError,
|
||||||
|
isAuthenticationError,
|
||||||
|
classifyError,
|
||||||
|
getUserFriendlyErrorMessage,
|
||||||
|
} from './error-handler';
|
||||||
|
|
||||||
|
// Conversation utilities
|
||||||
|
export {
|
||||||
|
extractTextFromContent,
|
||||||
|
normalizeContentBlocks,
|
||||||
|
formatHistoryAsText,
|
||||||
|
convertHistoryToMessages,
|
||||||
|
} from './conversation-utils';
|
||||||
|
|
||||||
|
// Image handling
|
||||||
|
export {
|
||||||
|
getMimeTypeForImage,
|
||||||
|
readImageAsBase64,
|
||||||
|
convertImagesToContentBlocks,
|
||||||
|
formatImagePathsForPrompt,
|
||||||
|
} from './image-handler';
|
||||||
|
|
||||||
|
// Prompt building
|
||||||
|
export {
|
||||||
|
buildPromptWithImages,
|
||||||
|
type PromptContent,
|
||||||
|
type PromptWithImages,
|
||||||
|
} from './prompt-builder';
|
||||||
|
|
||||||
|
// Logger
|
||||||
|
export {
|
||||||
|
createLogger,
|
||||||
|
getLogLevel,
|
||||||
|
setLogLevel,
|
||||||
|
LogLevel,
|
||||||
|
} from './logger';
|
||||||
|
|
||||||
|
// File system utilities
|
||||||
|
export {
|
||||||
|
mkdirSafe,
|
||||||
|
existsSafe,
|
||||||
|
} from './fs-utils';
|
||||||
74
libs/utils/src/logger.ts
Normal file
74
libs/utils/src/logger.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Simple logger utility with log levels
|
||||||
|
* Configure via LOG_LEVEL environment variable: error, warn, info, debug
|
||||||
|
* Defaults to 'info' if not set
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
ERROR = 0,
|
||||||
|
WARN = 1,
|
||||||
|
INFO = 2,
|
||||||
|
DEBUG = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_LEVEL_NAMES: Record<string, LogLevel> = {
|
||||||
|
error: LogLevel.ERROR,
|
||||||
|
warn: LogLevel.WARN,
|
||||||
|
info: LogLevel.INFO,
|
||||||
|
debug: LogLevel.DEBUG,
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentLogLevel: LogLevel = LogLevel.INFO;
|
||||||
|
|
||||||
|
// Initialize log level from environment variable
|
||||||
|
const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
|
||||||
|
if (envLogLevel && LOG_LEVEL_NAMES[envLogLevel] !== undefined) {
|
||||||
|
currentLogLevel = LOG_LEVEL_NAMES[envLogLevel];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a logger instance with a context prefix
|
||||||
|
*/
|
||||||
|
export function createLogger(context: string) {
|
||||||
|
const prefix = `[${context}]`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: (...args: unknown[]): void => {
|
||||||
|
if (currentLogLevel >= LogLevel.ERROR) {
|
||||||
|
console.error(prefix, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
warn: (...args: unknown[]): void => {
|
||||||
|
if (currentLogLevel >= LogLevel.WARN) {
|
||||||
|
console.warn(prefix, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
info: (...args: unknown[]): void => {
|
||||||
|
if (currentLogLevel >= LogLevel.INFO) {
|
||||||
|
console.log(prefix, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
debug: (...args: unknown[]): void => {
|
||||||
|
if (currentLogLevel >= LogLevel.DEBUG) {
|
||||||
|
console.log(prefix, "[DEBUG]", ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current log level
|
||||||
|
*/
|
||||||
|
export function getLogLevel(): LogLevel {
|
||||||
|
return currentLogLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the log level programmatically (useful for testing)
|
||||||
|
*/
|
||||||
|
export function setLogLevel(level: LogLevel): void {
|
||||||
|
currentLogLevel = level;
|
||||||
|
}
|
||||||
79
libs/utils/src/prompt-builder.ts
Normal file
79
libs/utils/src/prompt-builder.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Prompt building utilities for constructing prompts with images
|
||||||
|
*
|
||||||
|
* Provides standardized prompt building that:
|
||||||
|
* - Combines text prompts with image attachments
|
||||||
|
* - Handles content block array generation
|
||||||
|
* - Optionally includes image paths in text
|
||||||
|
* - Supports both vision and non-vision models
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { convertImagesToContentBlocks, formatImagePathsForPrompt } from "./image-handler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content that can be either simple text or structured blocks
|
||||||
|
*/
|
||||||
|
export type PromptContent = string | Array<{
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
source?: object;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of building a prompt with optional images
|
||||||
|
*/
|
||||||
|
export interface PromptWithImages {
|
||||||
|
content: PromptContent;
|
||||||
|
hasImages: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a prompt with optional image attachments
|
||||||
|
*
|
||||||
|
* @param basePrompt - The text prompt
|
||||||
|
* @param imagePaths - Optional array of image file paths
|
||||||
|
* @param workDir - Optional working directory for resolving relative paths
|
||||||
|
* @param includeImagePaths - Whether to append image paths to the text (default: false)
|
||||||
|
* @returns Promise resolving to prompt content and metadata
|
||||||
|
*/
|
||||||
|
export async function buildPromptWithImages(
|
||||||
|
basePrompt: string,
|
||||||
|
imagePaths?: string[],
|
||||||
|
workDir?: string,
|
||||||
|
includeImagePaths: boolean = false
|
||||||
|
): Promise<PromptWithImages> {
|
||||||
|
// No images - return plain text
|
||||||
|
if (!imagePaths || imagePaths.length === 0) {
|
||||||
|
return { content: basePrompt, hasImages: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build text content with optional image path listing
|
||||||
|
let textContent = basePrompt;
|
||||||
|
if (includeImagePaths) {
|
||||||
|
textContent += formatImagePathsForPrompt(imagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build content blocks array
|
||||||
|
const contentBlocks: Array<{
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
source?: object;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Add text block if we have text
|
||||||
|
if (textContent.trim()) {
|
||||||
|
contentBlocks.push({ type: "text", text: textContent });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add image blocks
|
||||||
|
const imageBlocks = await convertImagesToContentBlocks(imagePaths, workDir);
|
||||||
|
contentBlocks.push(...imageBlocks);
|
||||||
|
|
||||||
|
// Return appropriate format
|
||||||
|
const content: PromptContent =
|
||||||
|
contentBlocks.length > 1 || contentBlocks[0]?.type === "image"
|
||||||
|
? contentBlocks
|
||||||
|
: textContent;
|
||||||
|
|
||||||
|
return { content, hasImages: true };
|
||||||
|
}
|
||||||
20
libs/utils/tsconfig.json
Normal file
20
libs/utils/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"types": ["node"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user