New tools added across multiple domains: - Sales: lead-score, proposal-outline, objection-response - Marketing: competitor-brief, campaign-brief, social-post-draft, email-subject-score, audience-persona, content-calendar-plan, pricing-page-copy - HR: job-description-draft, interview-questions, performance-review-draft, onboarding-checklist, compensation-band, survey-analyze, org-chart-format, offer-letter-draft, exit-interview-summarize, policy-doc-format - Legal: contract-clause-scan, nda-template-draft, tos-readability, risk-clause-highlight, invoice-terms-extract, gdpr-data-map, copyright-notice, trademark-check - Finance: expense-categorize, invoice-data-extract, budget-variance, cash-flow-project, revenue-breakdown, ratio-analysis, tax-deduction-scan, reconciliation-match - Customer Experience: feedback-themes, churn-risk-score, nps-analysis, ticket-categorize, response-template-suggest, health-score-calculate, renewal-forecast - Education: lesson-plan-outline, quiz-generate, rubric-create, syllabus-format, progress-report-draft, learning-objective-write, curriculum-map Also includes improvements to 68 existing tool implementations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
227 lines
6.5 KiB
TypeScript
227 lines
6.5 KiB
TypeScript
/**
|
|
* Rows Join Tool for TPMJS
|
|
* Joins two arrays of objects by key fields, supporting inner, left, right, and full outer joins
|
|
*
|
|
* Domain rule: relational_joins - Supports SQL-like join operations (inner, left, right, full)
|
|
* Domain rule: field_prefixing - Prefixes fields with 'left_' and 'right_' to avoid collisions
|
|
*/
|
|
|
|
import { jsonSchema, tool } from 'ai';
|
|
|
|
/**
|
|
* Supported join types
|
|
*/
|
|
export type JoinType = 'inner' | 'left' | 'right' | 'full';
|
|
|
|
/**
|
|
* Result of joining two arrays
|
|
*/
|
|
export interface JoinResult {
|
|
rows: Array<Record<string, any>>;
|
|
matchedCount: number;
|
|
unmatchedLeft: number;
|
|
unmatchedRight: number;
|
|
}
|
|
|
|
type RowsJoinInput = {
|
|
left: Array<Record<string, any>>;
|
|
right: Array<Record<string, any>>;
|
|
leftKey: string;
|
|
rightKey: string;
|
|
type?: JoinType;
|
|
};
|
|
|
|
/**
|
|
* Domain rule: nested_field_access - Gets the value from an object by field path with dot notation
|
|
*/
|
|
function getFieldValue(obj: Record<string, any>, field: string): any {
|
|
const parts = field.split('.');
|
|
let value: any = obj;
|
|
for (const part of parts) {
|
|
if (value === null || value === undefined) {
|
|
return undefined;
|
|
}
|
|
value = value[part];
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Converts a value to a string key for matching
|
|
*/
|
|
function toKey(value: any): string {
|
|
if (value === null || value === undefined) {
|
|
return '__NULL__';
|
|
}
|
|
if (typeof value === 'object') {
|
|
return JSON.stringify(value);
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
/**
|
|
* Domain rule: field_prefixing - Merges two objects, prefixing keys to avoid collisions
|
|
*/
|
|
function mergeRows(
|
|
leftRow: Record<string, any> | null,
|
|
rightRow: Record<string, any> | null,
|
|
leftKey: string,
|
|
rightKey: string
|
|
): Record<string, any> {
|
|
const result: Record<string, any> = {};
|
|
|
|
// Add left row fields with 'left_' prefix
|
|
if (leftRow) {
|
|
for (const [key, value] of Object.entries(leftRow)) {
|
|
result[`left_${key}`] = value;
|
|
}
|
|
}
|
|
|
|
// Add right row fields with 'right_' prefix
|
|
if (rightRow) {
|
|
for (const [key, value] of Object.entries(rightRow)) {
|
|
result[`right_${key}`] = value;
|
|
}
|
|
}
|
|
|
|
// Add the join key without prefix for convenience
|
|
if (leftRow && rightRow) {
|
|
const joinValue = getFieldValue(leftRow, leftKey);
|
|
result.joinKey = joinValue;
|
|
} else if (leftRow) {
|
|
result.joinKey = getFieldValue(leftRow, leftKey);
|
|
} else if (rightRow) {
|
|
result.joinKey = getFieldValue(rightRow, rightKey);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Rows Join Tool
|
|
* Joins two arrays of objects by key fields
|
|
*/
|
|
export const rowsJoinTool = tool({
|
|
description:
|
|
'Joins two arrays of objects by matching key fields. Supports inner join (only matches), left join (all left + matches), right join (all right + matches), and full outer join (all from both). Fields are prefixed with left_ and right_ to avoid collisions.',
|
|
inputSchema: jsonSchema<RowsJoinInput>({
|
|
type: 'object',
|
|
properties: {
|
|
left: {
|
|
type: 'array',
|
|
description: 'Left array of objects to join',
|
|
items: {
|
|
type: 'object',
|
|
},
|
|
},
|
|
right: {
|
|
type: 'array',
|
|
description: 'Right array of objects to join',
|
|
items: {
|
|
type: 'object',
|
|
},
|
|
},
|
|
leftKey: {
|
|
type: 'string',
|
|
description:
|
|
'Field name in left array to join on (supports nested fields with dot notation)',
|
|
},
|
|
rightKey: {
|
|
type: 'string',
|
|
description:
|
|
'Field name in right array to join on (supports nested fields with dot notation)',
|
|
},
|
|
type: {
|
|
type: 'string',
|
|
enum: ['inner', 'left', 'right', 'full'],
|
|
description:
|
|
"Join type: 'inner' (only matches), 'left' (all left + matches), 'right' (all right + matches), 'full' (all from both). Defaults to 'inner'.",
|
|
default: 'inner',
|
|
},
|
|
},
|
|
required: ['left', 'right', 'leftKey', 'rightKey'],
|
|
additionalProperties: false,
|
|
}),
|
|
async execute({ left, right, leftKey, rightKey, type = 'inner' }): Promise<JoinResult> {
|
|
// Validate inputs
|
|
if (!Array.isArray(left)) {
|
|
throw new Error('left must be an array');
|
|
}
|
|
|
|
if (!Array.isArray(right)) {
|
|
throw new Error('right must be an array');
|
|
}
|
|
|
|
if (!leftKey || typeof leftKey !== 'string') {
|
|
throw new Error('leftKey must be a non-empty string');
|
|
}
|
|
|
|
if (!rightKey || typeof rightKey !== 'string') {
|
|
throw new Error('rightKey must be a non-empty string');
|
|
}
|
|
|
|
if (!['inner', 'left', 'right', 'full'].includes(type)) {
|
|
throw new Error(`Invalid join type "${type}". Must be one of: inner, left, right, full`);
|
|
}
|
|
|
|
// Domain rule: relational_joins - Build index for right array to enable efficient matching
|
|
const rightIndex = new Map<string, Array<Record<string, any>>>();
|
|
for (const rightRow of right) {
|
|
const key = toKey(getFieldValue(rightRow, rightKey));
|
|
if (!rightIndex.has(key)) {
|
|
rightIndex.set(key, []);
|
|
}
|
|
rightIndex.get(key)?.push(rightRow);
|
|
}
|
|
|
|
const results: Array<Record<string, any>> = [];
|
|
const matchedLeftKeys = new Set<string>();
|
|
const matchedRightKeys = new Set<string>();
|
|
let matchedCount = 0;
|
|
|
|
// Process left array
|
|
for (const leftRow of left) {
|
|
const key = toKey(getFieldValue(leftRow, leftKey));
|
|
const rightMatches = rightIndex.get(key);
|
|
|
|
if (rightMatches && rightMatches.length > 0) {
|
|
// Found match(es)
|
|
matchedLeftKeys.add(key);
|
|
for (const rightRow of rightMatches) {
|
|
results.push(mergeRows(leftRow, rightRow, leftKey, rightKey));
|
|
matchedRightKeys.add(key);
|
|
matchedCount++;
|
|
}
|
|
} else if (type === 'left' || type === 'full') {
|
|
// No match, but include for left/full join
|
|
results.push(mergeRows(leftRow, null, leftKey, rightKey));
|
|
}
|
|
}
|
|
|
|
// For right and full joins, add unmatched right rows
|
|
if (type === 'right' || type === 'full') {
|
|
for (const rightRow of right) {
|
|
const key = toKey(getFieldValue(rightRow, rightKey));
|
|
if (!matchedRightKeys.has(key)) {
|
|
results.push(mergeRows(null, rightRow, leftKey, rightKey));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate unmatched counts
|
|
const unmatchedLeft =
|
|
type === 'left' || type === 'full' ? left.length - Array.from(matchedLeftKeys).length : 0;
|
|
|
|
const unmatchedRight =
|
|
type === 'right' || type === 'full' ? right.length - Array.from(matchedRightKeys).length : 0;
|
|
|
|
return {
|
|
rows: results,
|
|
matchedCount,
|
|
unmatchedLeft,
|
|
unmatchedRight,
|
|
};
|
|
},
|
|
});
|
|
|
|
export default rowsJoinTool;
|