@aigne/agent-library 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,9 @@
1
- ## [1.2.0](https://github.com/AIGNE-io/aigne-framework/compare/agent-library-v1.1.0...agent-library-v1.2.0) (2025-03-18)
1
+ ## [1.11.0](https://github.com/AIGNE-io/aigne-framework/compare/agent-library-v1.10.0...agent-library-v1.11.0) (2025-05-27)
2
2
 
3
- - chore: release 1.2.0
3
+
4
+ ### Features
5
+
6
+ * add schema transform ([#35](https://github.com/AIGNE-io/aigne-framework/issues/35)) ([c7d9a2c](https://github.com/AIGNE-io/aigne-framework/commit/c7d9a2c9fcab8d384d4198db5ff6ba4603846cdf))
4
7
 
5
8
  ## [1.10.0](https://github.com/AIGNE-io/aigne-framework/compare/agent-library-v1.9.0...agent-library-v1.10.0) (2025-05-25)
6
9
 
@@ -0,0 +1,14 @@
1
+ import { AIAgent } from "@aigne/core";
2
+ declare const mapper: AIAgent<{
3
+ sourceData: string;
4
+ responseSchema: string;
5
+ sourceSchema?: string | undefined;
6
+ instruction?: string | undefined;
7
+ responseData?: string | undefined;
8
+ feedback?: string | undefined;
9
+ }, {
10
+ jsonata: string;
11
+ confidence: number;
12
+ confidenceReasoning: string;
13
+ }>;
14
+ export default mapper;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@aigne/core");
4
+ const zod_1 = require("zod");
5
+ const prompts_js_1 = require("../prompts.js");
6
+ const mapper = core_1.AIAgent.from({
7
+ subscribeTopic: [core_1.UserInputTopic, "mapping_request"],
8
+ publishTopic: "review_request",
9
+ inputSchema: zod_1.z.object({
10
+ sourceData: zod_1.z.string(),
11
+ sourceSchema: zod_1.z.string().optional(),
12
+ responseSchema: zod_1.z.string(),
13
+ instruction: zod_1.z.string().optional(),
14
+ responseData: zod_1.z.string().optional(),
15
+ feedback: zod_1.z.string().optional(),
16
+ }),
17
+ outputSchema: zod_1.z.object({
18
+ jsonata: zod_1.z.string().describe("JSONata expression"),
19
+ confidence: zod_1.z.number().describe(`Confidence score for the JSONata expression between 0 and 100.
20
+ Give a low confidence score if there are missing fields in the source data.
21
+ Give a low confidence score if there are multiple options for a field and it is unclear which one to choose.`),
22
+ confidenceReasoning: zod_1.z.string().describe("Reasoning for the confidence score"),
23
+ }),
24
+ includeInputInOutput: true,
25
+ instructions: core_1.PromptBuilder.from({
26
+ messages: [
27
+ {
28
+ role: "assistant",
29
+ content: {
30
+ type: "text",
31
+ text: prompts_js_1.PROMPT_MAPPING,
32
+ },
33
+ },
34
+ {
35
+ role: "user",
36
+ content: {
37
+ type: "text",
38
+ text: `Given a source data and structure, create a jsonata expression in JSON FORMAT.
39
+ Important: The output should be a jsonata expression creating an object that matches the following schema:
40
+ {{responseSchema}}
41
+
42
+ Pay special attention to the requirements in field descriptions
43
+
44
+ The instruction from the user is: {{instruction}}
45
+
46
+ ------
47
+
48
+ Source Data Structure:
49
+ {{sourceSchema}}
50
+
51
+ Source data Sample:
52
+ {{sourceData}}
53
+
54
+ ------
55
+
56
+ Previous feedback:
57
+ {{feedback}}`,
58
+ },
59
+ },
60
+ ],
61
+ }),
62
+ });
63
+ exports.default = mapper;
@@ -0,0 +1,19 @@
1
+ import { FunctionAgent } from "@aigne/core";
2
+ declare const reviewer: FunctionAgent<{
3
+ sourceData: string;
4
+ responseSchema: string;
5
+ jsonata: string;
6
+ }, {
7
+ success: boolean;
8
+ data: null;
9
+ feedback: string;
10
+ } | {
11
+ success: boolean;
12
+ data: unknown;
13
+ feedback?: undefined;
14
+ } | {
15
+ success: false;
16
+ data: unknown;
17
+ feedback: string | undefined;
18
+ }>;
19
+ export default reviewer;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@aigne/core");
4
+ const zod_1 = require("zod");
5
+ const tools_js_1 = require("../tools.js");
6
+ const reviewer = core_1.FunctionAgent.from({
7
+ name: "check_mapping",
8
+ description: "Check the mapping result",
9
+ subscribeTopic: ["review_request"],
10
+ publishTopic: (output) => (output.success ? core_1.UserOutputTopic : "mapping_request"),
11
+ inputSchema: zod_1.z.object({
12
+ sourceData: zod_1.z.string(),
13
+ jsonata: zod_1.z.string(),
14
+ responseSchema: zod_1.z.string(),
15
+ }),
16
+ process: async ({ sourceData, jsonata, responseSchema, }) => {
17
+ let parsedSourceData = null;
18
+ let parsedResponseSchema = null;
19
+ try {
20
+ parsedSourceData = sourceData ? JSON.parse(sourceData) : null;
21
+ parsedResponseSchema = responseSchema ? JSON.parse(responseSchema) : null;
22
+ }
23
+ catch (parseError) {
24
+ // input data is not valid JSON, return success
25
+ return {
26
+ success: true,
27
+ data: null,
28
+ feedback: `JSON parsing failed: ${parseError.message}`,
29
+ };
30
+ }
31
+ const transformation = await (0, tools_js_1.applyJsonataWithValidation)(parsedSourceData, jsonata, parsedResponseSchema);
32
+ // if transformation is successful, return success
33
+ if (transformation.success) {
34
+ return {
35
+ success: true,
36
+ data: transformation.data,
37
+ };
38
+ }
39
+ return {
40
+ success: transformation.success,
41
+ data: transformation.data,
42
+ feedback: transformation.error,
43
+ };
44
+ },
45
+ includeInputInOutput: true,
46
+ });
47
+ exports.default = reviewer;
@@ -0,0 +1,18 @@
1
+ import { type ChatModel } from "@aigne/core";
2
+ export interface TransformInput {
3
+ responseSchema: string;
4
+ responseSampleData?: string;
5
+ sourceData?: string;
6
+ sourceSchema?: string;
7
+ instruction?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ export declare function generateMapping({ input, model, }: {
11
+ input: TransformInput;
12
+ model: ChatModel;
13
+ }): Promise<{
14
+ jsonata: string;
15
+ confidence: number;
16
+ confidenceReasoning: string;
17
+ } | null>;
18
+ export { applyJsonata } from "./tools.js";
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyJsonata = void 0;
7
+ exports.generateMapping = generateMapping;
8
+ const core_1 = require("@aigne/core");
9
+ const mapper_js_1 = __importDefault(require("./agents/mapper.js"));
10
+ const reviewer_js_1 = __importDefault(require("./agents/reviewer.js"));
11
+ const tools_js_1 = require("./tools.js");
12
+ async function generateMapping({ input, model, }) {
13
+ if (!model)
14
+ throw new Error("model is required to run data mapper");
15
+ // if sourceSchema is not provided, generate it from sourceData
16
+ if (!input.sourceSchema && input.sourceData) {
17
+ input.sourceSchema = JSON.stringify((0, tools_js_1.getSchemaFromData)(JSON.parse(input.sourceData)));
18
+ }
19
+ const aigne = new core_1.AIGNE({ model, agents: [mapper_js_1.default, reviewer_js_1.default] });
20
+ aigne.publish(core_1.UserInputTopic, input);
21
+ const { message } = await aigne.subscribe(core_1.UserOutputTopic);
22
+ return {
23
+ jsonata: message.jsonata || "",
24
+ confidence: message.confidence || 0,
25
+ confidenceReasoning: message.confidenceReasoning || "",
26
+ };
27
+ }
28
+ var tools_js_2 = require("./tools.js");
29
+ Object.defineProperty(exports, "applyJsonata", { enumerable: true, get: function () { return tools_js_2.applyJsonata; } });
@@ -0,0 +1 @@
1
+ export declare const PROMPT_MAPPING = "You are an AI that generates JSONata mapping expressions to transform source data structures into target structures.\n\nGuidelines for creating JSONata mappings:\n\n1. Source References:\n - Use exact field paths from the source data, e.g. $.merchant_category\n - For accessing fields with names containing spaces, use backticks, e.g. $.`merchant category`\n - Jsonata will automatically extract all the fields from the current context. E.g. if you need all variants from all products, you can use $.products.variants. No need to do nested map reduce operations.\n - $. The variable with no name refers to the context value at any point in the input JSON hierarchy. E.g. if the current context is products.price, then $.currency is products.price.currency\n - %. The parent of the current context value. E.g. if the current context is products.variants.size and you want variant name, use %.name\n\n - When multiple source fields could map to a target, use a maximum of 3 fallbacks:\n GOOD: source1 ? source1 : source2 ? source2 : source3 ? source3 : 'default'\n BAD: source1 ? source1 : source1 ? source1 : source1 (repeated fields)\n\n2. Expression Rules:\n - Avoid unnecessary array/string operations\n - Each mapping should be clear and concise\n - Use proper JSONata syntax for coalesce operations\n - Do not use ~> to execute functions. Use the functions directly with the correct arguments or use $map(arr, $function) to apply a function to each element of an array.\n\n3. Array Handling:\n - For mapping to an array of objects, use the following patterns:\n a) When in array scope, use $.{} to map each object:\n Correct: [$.{\"id\": id, \"name\": name}]\n Incorrect: [{\"id\": $.id}]\n b) When outside array scope, include the source path:\n Correct: [$.items.{\"id\": id, \"name\": name}]\n Incorrect: [{\"id\": $.items.id}]\n c) For nested arrays, chain the array operators:\n Correct: [products.variants.{\"size\": size, \"color\": color}]\n Incorrect: [products.[{\"size\": variants.size}]]\n d) You need to use the square brackets [] to map to an array of objects, otherwise it might return an object and fail the validation.\n Correct: variants: [variants.{\"size\": size, \"color\": color}]\n Incorrect: variants: variants.{\"size\": variants.size}\n - For array elements, use JSONata array operators like [0] for first element, [-1] for last element\n - Square bracket notation [] can be used with predicates, e.g. items[type='book']\n\n4. Field Selection Priority:\n - Prefer variant-specific fields over general fields (e.g., sizeVariants.description over sizes)\n - Choose the most specific/detailed field available (e.g., type=\"shelf\" over category=\"furniture\")\n\n5. Filters:\n - Pay special attention to filter statements in the instruction and the schema description. Add them to the generated jsonata expression.\n Example: Get me all products with SKU 0406654608 or products: {\"type\": \"array\", description: \"only products with SKU 0406654608\"}\n Generated jsonata expression: Account.Order.Product[SKU = \"0406654608\"].{\"Description\": Description}\n - For filtering with arrays, you can use the \"in\" operator. E.g. library.books[\"Steven King\" in authors]\n\n6. Data Integrity:\n - ONLY use fields that exist in the source data structure\n - If no matching source field exists, leave the field undefined\n - Never invent or assume the existence of fields not present in the source data\n\n7. Function Calls:\n - You may use the following functions if prompted:\n $string(arg) - Converts argument to string\n $length(str) - Returns string length\n $substring(str, start[, length]) - Extracts substring\n $substringBefore(str, chars) - Gets substring before specified chars\n $substringAfter(str, chars) - Gets substring after specified chars\n $uppercase(str) - Converts to uppercase\n $lowercase(str) - Converts to lowercase\n $trim(str) - Removes whitespace from both ends\n $pad(str, width[, char]) - Pads string to specified width\n $contains(str, substring) - Tests if string contains substring\n $toMillis(timestamp [, picture]) - Converts ISO 8601 timestamp to milliseconds. E.g. $toMillis(\"2017-11-07T15:07:54.972Z\") => 1510067274972\n $toDate(str | number) - Converts any timestamp string to valid ISO 8601 date string. E.g. $toDate(\"Oct 15, 2024 12:00:00 AM UTC\") => \"2024-10-15T00:00:00.000Z\", $toDate(1728873600000) => \"2024-10-15T00:00:00.000Z\"\n $dateMax(arr) - Returns the maximum date of an array of dates. E.g. $dateMax([\"2017-11-07T15:07:54.972Z\", \"Oct 15, 2012 12:00:00 AM UTC\"]) returns \"2017-11-07T15:07:54.972Z\".\n $dateMin(arr) - Returns the minimum date of an array of dates. E.g. $dateMin($.variants.created_at) returns the minimum created_at date of all variants.\n $dateDiff(date1, date2, unit: \"seconds\" | \"minutes\" | \"hours\" | \"days\") - Returns the difference between two dates in the specified unit. E.g. $dateDiff($.order.created_at, $.order.updated_at, \"days\") returns the number of days between the order created_at and updated_at.\n $now([picture [, timezone]]) - Returns current date and time in ISO 8601 format. E.g. $now() => \"2017-05-15T15:12:59.152Z\"\n $split(str[, separator][, limit]) - Splits string into array\n $join(array[, separator]) - Joins array elements into string\n $match(str, pattern[, limit]) - Returns array of regex matches\n $replace(str, pattern, replacement) - Replaces all occurrences of pattern. E.g. $replace(\"abracadabra\", /a.*?a/, \"*\") returns \"ab*ad*bra\". $replace(\"John Smith\", \"John\", \"Marc\") returns Marc Smith.\n $number(arg) - Converts an argument to a number.\n $min(arr) - Returns minimum number of a number array. E.g. $min($map($.variants.price, $number)) returns the minimum price of all variants.\n $max(arr) - Returns maximum number of a number array. E.g. $max($map($.variants.price, $number)) returns the maximum price of all variants.\n $count(array) - Returns array length\n $sort(array[, function]) - Sorts array\n $distinct(array) - Removes duplicates\n $map(array, function) - Applies function to each element\n $filter(array, function) - Filters array based on predicate\n\n- Error handling:\n - If you get an error like \"is not of a type(s) string/number/object\", try to convert the source field, but also consider that the original field or one of its parent might be null. In this case, add a default value.\n - If the error is something like \"instance is not of a type(s) object\", make sure you REALLY create the target schema with the correct type.\n - If the error is something like \"instance is not of a type(s) array or array/null\". In this case, wrap the source selector in an array to ensure it always returns an array. E.g. \"result\": [$.items]\n - if an object is optional but its fields required, you can add a test and default to {}, but do not set the inner fields to default null.\n\nRemember: The goal is to create valid JSONata expressions that accurately transform the source data structure into the required target structure.";
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROMPT_MAPPING = void 0;
4
+ exports.PROMPT_MAPPING = `You are an AI that generates JSONata mapping expressions to transform source data structures into target structures.
5
+
6
+ Guidelines for creating JSONata mappings:
7
+
8
+ 1. Source References:
9
+ - Use exact field paths from the source data, e.g. $.merchant_category
10
+ - For accessing fields with names containing spaces, use backticks, e.g. $.\`merchant category\`
11
+ - Jsonata will automatically extract all the fields from the current context. E.g. if you need all variants from all products, you can use $.products.variants. No need to do nested map reduce operations.
12
+ - $. The variable with no name refers to the context value at any point in the input JSON hierarchy. E.g. if the current context is products.price, then $.currency is products.price.currency
13
+ - %. The parent of the current context value. E.g. if the current context is products.variants.size and you want variant name, use %.name
14
+
15
+ - When multiple source fields could map to a target, use a maximum of 3 fallbacks:
16
+ GOOD: source1 ? source1 : source2 ? source2 : source3 ? source3 : 'default'
17
+ BAD: source1 ? source1 : source1 ? source1 : source1 (repeated fields)
18
+
19
+ 2. Expression Rules:
20
+ - Avoid unnecessary array/string operations
21
+ - Each mapping should be clear and concise
22
+ - Use proper JSONata syntax for coalesce operations
23
+ - Do not use ~> to execute functions. Use the functions directly with the correct arguments or use $map(arr, $function) to apply a function to each element of an array.
24
+
25
+ 3. Array Handling:
26
+ - For mapping to an array of objects, use the following patterns:
27
+ a) When in array scope, use $.{} to map each object:
28
+ Correct: [$.{"id": id, "name": name}]
29
+ Incorrect: [{"id": $.id}]
30
+ b) When outside array scope, include the source path:
31
+ Correct: [$.items.{"id": id, "name": name}]
32
+ Incorrect: [{"id": $.items.id}]
33
+ c) For nested arrays, chain the array operators:
34
+ Correct: [products.variants.{"size": size, "color": color}]
35
+ Incorrect: [products.[{"size": variants.size}]]
36
+ d) You need to use the square brackets [] to map to an array of objects, otherwise it might return an object and fail the validation.
37
+ Correct: variants: [variants.{"size": size, "color": color}]
38
+ Incorrect: variants: variants.{"size": variants.size}
39
+ - For array elements, use JSONata array operators like [0] for first element, [-1] for last element
40
+ - Square bracket notation [] can be used with predicates, e.g. items[type='book']
41
+
42
+ 4. Field Selection Priority:
43
+ - Prefer variant-specific fields over general fields (e.g., sizeVariants.description over sizes)
44
+ - Choose the most specific/detailed field available (e.g., type="shelf" over category="furniture")
45
+
46
+ 5. Filters:
47
+ - Pay special attention to filter statements in the instruction and the schema description. Add them to the generated jsonata expression.
48
+ Example: Get me all products with SKU 0406654608 or products: {"type": "array", description: "only products with SKU 0406654608"}
49
+ Generated jsonata expression: Account.Order.Product[SKU = "0406654608"].{"Description": Description}
50
+ - For filtering with arrays, you can use the "in" operator. E.g. library.books["Steven King" in authors]
51
+
52
+ 6. Data Integrity:
53
+ - ONLY use fields that exist in the source data structure
54
+ - If no matching source field exists, leave the field undefined
55
+ - Never invent or assume the existence of fields not present in the source data
56
+
57
+ 7. Function Calls:
58
+ - You may use the following functions if prompted:
59
+ $string(arg) - Converts argument to string
60
+ $length(str) - Returns string length
61
+ $substring(str, start[, length]) - Extracts substring
62
+ $substringBefore(str, chars) - Gets substring before specified chars
63
+ $substringAfter(str, chars) - Gets substring after specified chars
64
+ $uppercase(str) - Converts to uppercase
65
+ $lowercase(str) - Converts to lowercase
66
+ $trim(str) - Removes whitespace from both ends
67
+ $pad(str, width[, char]) - Pads string to specified width
68
+ $contains(str, substring) - Tests if string contains substring
69
+ $toMillis(timestamp [, picture]) - Converts ISO 8601 timestamp to milliseconds. E.g. $toMillis("2017-11-07T15:07:54.972Z") => 1510067274972
70
+ $toDate(str | number) - Converts any timestamp string to valid ISO 8601 date string. E.g. $toDate("Oct 15, 2024 12:00:00 AM UTC") => "2024-10-15T00:00:00.000Z", $toDate(1728873600000) => "2024-10-15T00:00:00.000Z"
71
+ $dateMax(arr) - Returns the maximum date of an array of dates. E.g. $dateMax(["2017-11-07T15:07:54.972Z", "Oct 15, 2012 12:00:00 AM UTC"]) returns "2017-11-07T15:07:54.972Z".
72
+ $dateMin(arr) - Returns the minimum date of an array of dates. E.g. $dateMin($.variants.created_at) returns the minimum created_at date of all variants.
73
+ $dateDiff(date1, date2, unit: "seconds" | "minutes" | "hours" | "days") - Returns the difference between two dates in the specified unit. E.g. $dateDiff($.order.created_at, $.order.updated_at, "days") returns the number of days between the order created_at and updated_at.
74
+ $now([picture [, timezone]]) - Returns current date and time in ISO 8601 format. E.g. $now() => "2017-05-15T15:12:59.152Z"
75
+ $split(str[, separator][, limit]) - Splits string into array
76
+ $join(array[, separator]) - Joins array elements into string
77
+ $match(str, pattern[, limit]) - Returns array of regex matches
78
+ $replace(str, pattern, replacement) - Replaces all occurrences of pattern. E.g. $replace("abracadabra", /a.*?a/, "*") returns "ab*ad*bra". $replace("John Smith", "John", "Marc") returns Marc Smith.
79
+ $number(arg) - Converts an argument to a number.
80
+ $min(arr) - Returns minimum number of a number array. E.g. $min($map($.variants.price, $number)) returns the minimum price of all variants.
81
+ $max(arr) - Returns maximum number of a number array. E.g. $max($map($.variants.price, $number)) returns the maximum price of all variants.
82
+ $count(array) - Returns array length
83
+ $sort(array[, function]) - Sorts array
84
+ $distinct(array) - Removes duplicates
85
+ $map(array, function) - Applies function to each element
86
+ $filter(array, function) - Filters array based on predicate
87
+
88
+ - Error handling:
89
+ - If you get an error like "is not of a type(s) string/number/object", try to convert the source field, but also consider that the original field or one of its parent might be null. In this case, add a default value.
90
+ - If the error is something like "instance is not of a type(s) object", make sure you REALLY create the target schema with the correct type.
91
+ - If the error is something like "instance is not of a type(s) array or array/null". In this case, wrap the source selector in an array to ensure it always returns an array. E.g. "result": [$.items]
92
+ - if an object is optional but its fields required, you can add a test and default to {}, but do not set the inner fields to default null.
93
+
94
+ Remember: The goal is to create valid JSONata expressions that accurately transform the source data structure into the required target structure.`;
@@ -0,0 +1,17 @@
1
+ import jsonata from "jsonata";
2
+ import type { Schema } from "jsonschema";
3
+ export interface TransformResult {
4
+ success: boolean;
5
+ data?: unknown;
6
+ error?: string;
7
+ }
8
+ /**
9
+ * Extract JSON Schema from data
10
+ * @param data Any data
11
+ * @returns JSON Schema or null
12
+ */
13
+ export declare function getSchemaFromData(data: unknown): unknown;
14
+ export declare function applyJsonataWithValidation(data: unknown, expr: string, schema: unknown): Promise<TransformResult>;
15
+ export declare function applyJsonata(data: unknown, expr: string): Promise<unknown>;
16
+ export declare function extendJsonata(expr: string): jsonata.Expression;
17
+ export declare function addNullableToOptional(schema: Schema): Schema;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getSchemaFromData = getSchemaFromData;
7
+ exports.applyJsonataWithValidation = applyJsonataWithValidation;
8
+ exports.applyJsonata = applyJsonata;
9
+ exports.extendJsonata = extendJsonata;
10
+ exports.addNullableToOptional = addNullableToOptional;
11
+ /* eslint-disable @typescript-eslint/no-explicit-any */
12
+ const jsonata_1 = __importDefault(require("jsonata"));
13
+ const jsonschema_1 = require("jsonschema");
14
+ const to_json_schema_1 = __importDefault(require("to-json-schema"));
15
+ /**
16
+ * Extract JSON Schema from data
17
+ * @param data Any data
18
+ * @returns JSON Schema or null
19
+ */
20
+ function getSchemaFromData(data) {
21
+ if (!data)
22
+ return null;
23
+ return (0, to_json_schema_1.default)(data);
24
+ }
25
+ async function applyJsonataWithValidation(data, expr, schema) {
26
+ try {
27
+ const result = await applyJsonata(data, expr);
28
+ if (result === null ||
29
+ result === undefined ||
30
+ (Array.isArray(result) && result.length === 0) ||
31
+ (typeof result === "object" && Object.keys(result).length === 0)) {
32
+ return { success: false, error: "Result is empty" };
33
+ }
34
+ const validator = new jsonschema_1.Validator();
35
+ const optionalSchema = addNullableToOptional(schema);
36
+ const validation = validator.validate(result, optionalSchema);
37
+ if (!validation.valid) {
38
+ return {
39
+ success: false,
40
+ error: validation.errors
41
+ .map((e) => `${e.stack}. Source: ${e.instance ? JSON.stringify(e.instance) : "undefined"}`)
42
+ .join("\n")
43
+ .slice(0, 5000),
44
+ };
45
+ }
46
+ return { success: true, data: result };
47
+ }
48
+ catch (error) {
49
+ return { success: false, error: `Validation failed: ${error.message}` };
50
+ }
51
+ }
52
+ async function applyJsonata(data, expr) {
53
+ try {
54
+ const expression = extendJsonata(expr);
55
+ const result = await expression.evaluate(data);
56
+ return result;
57
+ }
58
+ catch (error) {
59
+ throw new Error(`JSONata evaluation failed for expression "${expr}": ${error.message}`);
60
+ }
61
+ }
62
+ function extendJsonata(expr) {
63
+ const expression = (0, jsonata_1.default)(expr);
64
+ expression.registerFunction("max", (arr) => {
65
+ if (Array.isArray(arr)) {
66
+ return Math.max(...arr.map(Number).filter((n) => !Number.isNaN(n)));
67
+ }
68
+ return arr;
69
+ });
70
+ expression.registerFunction("min", (arr) => {
71
+ if (Array.isArray(arr)) {
72
+ return Math.min(...arr.map(Number).filter((n) => !Number.isNaN(n)));
73
+ }
74
+ return arr;
75
+ });
76
+ expression.registerFunction("number", (value) => Number.parseFloat(value));
77
+ expression.registerFunction("substring", (str, start, end) => String(str).substring(start, end));
78
+ expression.registerFunction("replace", (obj, pattern, replacement) => {
79
+ if (Array.isArray(obj)) {
80
+ return obj.map((item) => String(item).replace(pattern, replacement));
81
+ }
82
+ if (typeof obj === "object") {
83
+ return Object.fromEntries(Object.entries(obj || {}).map(([key, value]) => [
84
+ key,
85
+ String(value).replace(pattern, replacement),
86
+ ]));
87
+ }
88
+ return String(obj).replace(pattern, replacement);
89
+ });
90
+ expression.registerFunction("toDate", (date) => {
91
+ try {
92
+ // Handle numeric timestamps (milliseconds or seconds)
93
+ if (typeof date === "number" || /^\d+$/.test(date)) {
94
+ const timestamp = typeof date === "number" ? date : Number.parseInt(date, 10);
95
+ // If timestamp is in seconds (typically 10 digits), convert to milliseconds
96
+ const millisTimestamp = timestamp < 10000000000 ? timestamp * 1000 : timestamp;
97
+ return new Date(millisTimestamp).toISOString();
98
+ }
99
+ // Handle date strings in MM/DD/YYYY format
100
+ const match = String(date).match(/^(\d{2})\/(\d{2})\/(\d{4})(?:\s+(\d{2}):(\d{2}):(\d{2}))?$/);
101
+ if (match) {
102
+ // eslint-disable-next-line @typescript-eslint/naming-convention
103
+ const [_, month, day, year, hours = "00", minutes = "00", seconds = "00"] = match;
104
+ const isoDate = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.000Z`;
105
+ return new Date(isoDate).toISOString();
106
+ }
107
+ // Default case: try standard Date parsing
108
+ return new Date(date).toISOString();
109
+ }
110
+ catch (e) {
111
+ throw new Error(`Invalid date: ${e.message}`);
112
+ }
113
+ });
114
+ expression.registerFunction("dateMax", (dates) => dates.reduce((max, curr) => (new Date(max) > new Date(curr) ? max : curr)));
115
+ expression.registerFunction("dateMin", (dates) => dates.reduce((min, curr) => (new Date(min) < new Date(curr) ? min : curr)));
116
+ expression.registerFunction("dateDiff", (date1, date2, unit = "days") => {
117
+ const d1 = new Date(date1);
118
+ const d2 = new Date(date2);
119
+ const diff = Math.abs(d1.getTime() - d2.getTime());
120
+ switch (unit.toLowerCase()) {
121
+ case "seconds":
122
+ return Math.floor(diff / 1000);
123
+ case "minutes":
124
+ return Math.floor(diff / (1000 * 60));
125
+ case "hours":
126
+ return Math.floor(diff / (1000 * 60 * 60));
127
+ case "days":
128
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
129
+ default:
130
+ return diff; // milliseconds
131
+ }
132
+ });
133
+ return expression;
134
+ }
135
+ function addNullableToOptional(schema) {
136
+ if (!schema || typeof schema !== "object")
137
+ return schema;
138
+ const newSchema = { ...schema };
139
+ if (schema.type === "object" && schema.properties) {
140
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
141
+ newSchema.properties = Object.entries(schema.properties).reduce((acc, [key, value]) => ({
142
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
143
+ ...acc,
144
+ [key]: !required.has(key) ? makeNullable(value) : addNullableToOptional(value),
145
+ }), {});
146
+ }
147
+ if (schema.type === "array" && schema.items) {
148
+ newSchema.items = addNullableToOptional(schema.items);
149
+ }
150
+ return newSchema;
151
+ }
152
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
153
+ function makeNullable(schema) {
154
+ if (!schema || typeof schema !== "object")
155
+ return schema;
156
+ const newSchema = { ...schema };
157
+ if (Array.isArray(schema.type)) {
158
+ if (!schema.type.includes("null")) {
159
+ newSchema.type = [...schema.type, "null"];
160
+ }
161
+ }
162
+ else if (schema.type) {
163
+ newSchema.type = [schema.type, "null"];
164
+ }
165
+ // Recursively process nested properties
166
+ if (schema.properties) {
167
+ newSchema.properties = Object.entries(schema.properties).reduce((acc, [key, value]) => ({
168
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
169
+ ...acc,
170
+ [key]: makeNullable(value),
171
+ }), {});
172
+ }
173
+ if (schema.items) {
174
+ newSchema.items = makeNullable(schema.items);
175
+ }
176
+ return newSchema;
177
+ }
@@ -0,0 +1,14 @@
1
+ import { AIAgent } from "@aigne/core";
2
+ declare const mapper: AIAgent<{
3
+ sourceData: string;
4
+ responseSchema: string;
5
+ sourceSchema?: string | undefined;
6
+ instruction?: string | undefined;
7
+ responseData?: string | undefined;
8
+ feedback?: string | undefined;
9
+ }, {
10
+ jsonata: string;
11
+ confidence: number;
12
+ confidenceReasoning: string;
13
+ }>;
14
+ export default mapper;
@@ -0,0 +1,19 @@
1
+ import { FunctionAgent } from "@aigne/core";
2
+ declare const reviewer: FunctionAgent<{
3
+ sourceData: string;
4
+ responseSchema: string;
5
+ jsonata: string;
6
+ }, {
7
+ success: boolean;
8
+ data: null;
9
+ feedback: string;
10
+ } | {
11
+ success: boolean;
12
+ data: unknown;
13
+ feedback?: undefined;
14
+ } | {
15
+ success: false;
16
+ data: unknown;
17
+ feedback: string | undefined;
18
+ }>;
19
+ export default reviewer;
@@ -0,0 +1,18 @@
1
+ import { type ChatModel } from "@aigne/core";
2
+ export interface TransformInput {
3
+ responseSchema: string;
4
+ responseSampleData?: string;
5
+ sourceData?: string;
6
+ sourceSchema?: string;
7
+ instruction?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ export declare function generateMapping({ input, model, }: {
11
+ input: TransformInput;
12
+ model: ChatModel;
13
+ }): Promise<{
14
+ jsonata: string;
15
+ confidence: number;
16
+ confidenceReasoning: string;
17
+ } | null>;
18
+ export { applyJsonata } from "./tools.js";
@@ -0,0 +1 @@
1
+ export declare const PROMPT_MAPPING = "You are an AI that generates JSONata mapping expressions to transform source data structures into target structures.\n\nGuidelines for creating JSONata mappings:\n\n1. Source References:\n - Use exact field paths from the source data, e.g. $.merchant_category\n - For accessing fields with names containing spaces, use backticks, e.g. $.`merchant category`\n - Jsonata will automatically extract all the fields from the current context. E.g. if you need all variants from all products, you can use $.products.variants. No need to do nested map reduce operations.\n - $. The variable with no name refers to the context value at any point in the input JSON hierarchy. E.g. if the current context is products.price, then $.currency is products.price.currency\n - %. The parent of the current context value. E.g. if the current context is products.variants.size and you want variant name, use %.name\n\n - When multiple source fields could map to a target, use a maximum of 3 fallbacks:\n GOOD: source1 ? source1 : source2 ? source2 : source3 ? source3 : 'default'\n BAD: source1 ? source1 : source1 ? source1 : source1 (repeated fields)\n\n2. Expression Rules:\n - Avoid unnecessary array/string operations\n - Each mapping should be clear and concise\n - Use proper JSONata syntax for coalesce operations\n - Do not use ~> to execute functions. Use the functions directly with the correct arguments or use $map(arr, $function) to apply a function to each element of an array.\n\n3. Array Handling:\n - For mapping to an array of objects, use the following patterns:\n a) When in array scope, use $.{} to map each object:\n Correct: [$.{\"id\": id, \"name\": name}]\n Incorrect: [{\"id\": $.id}]\n b) When outside array scope, include the source path:\n Correct: [$.items.{\"id\": id, \"name\": name}]\n Incorrect: [{\"id\": $.items.id}]\n c) For nested arrays, chain the array operators:\n Correct: [products.variants.{\"size\": size, \"color\": color}]\n Incorrect: [products.[{\"size\": variants.size}]]\n d) You need to use the square brackets [] to map to an array of objects, otherwise it might return an object and fail the validation.\n Correct: variants: [variants.{\"size\": size, \"color\": color}]\n Incorrect: variants: variants.{\"size\": variants.size}\n - For array elements, use JSONata array operators like [0] for first element, [-1] for last element\n - Square bracket notation [] can be used with predicates, e.g. items[type='book']\n\n4. Field Selection Priority:\n - Prefer variant-specific fields over general fields (e.g., sizeVariants.description over sizes)\n - Choose the most specific/detailed field available (e.g., type=\"shelf\" over category=\"furniture\")\n\n5. Filters:\n - Pay special attention to filter statements in the instruction and the schema description. Add them to the generated jsonata expression.\n Example: Get me all products with SKU 0406654608 or products: {\"type\": \"array\", description: \"only products with SKU 0406654608\"}\n Generated jsonata expression: Account.Order.Product[SKU = \"0406654608\"].{\"Description\": Description}\n - For filtering with arrays, you can use the \"in\" operator. E.g. library.books[\"Steven King\" in authors]\n\n6. Data Integrity:\n - ONLY use fields that exist in the source data structure\n - If no matching source field exists, leave the field undefined\n - Never invent or assume the existence of fields not present in the source data\n\n7. Function Calls:\n - You may use the following functions if prompted:\n $string(arg) - Converts argument to string\n $length(str) - Returns string length\n $substring(str, start[, length]) - Extracts substring\n $substringBefore(str, chars) - Gets substring before specified chars\n $substringAfter(str, chars) - Gets substring after specified chars\n $uppercase(str) - Converts to uppercase\n $lowercase(str) - Converts to lowercase\n $trim(str) - Removes whitespace from both ends\n $pad(str, width[, char]) - Pads string to specified width\n $contains(str, substring) - Tests if string contains substring\n $toMillis(timestamp [, picture]) - Converts ISO 8601 timestamp to milliseconds. E.g. $toMillis(\"2017-11-07T15:07:54.972Z\") => 1510067274972\n $toDate(str | number) - Converts any timestamp string to valid ISO 8601 date string. E.g. $toDate(\"Oct 15, 2024 12:00:00 AM UTC\") => \"2024-10-15T00:00:00.000Z\", $toDate(1728873600000) => \"2024-10-15T00:00:00.000Z\"\n $dateMax(arr) - Returns the maximum date of an array of dates. E.g. $dateMax([\"2017-11-07T15:07:54.972Z\", \"Oct 15, 2012 12:00:00 AM UTC\"]) returns \"2017-11-07T15:07:54.972Z\".\n $dateMin(arr) - Returns the minimum date of an array of dates. E.g. $dateMin($.variants.created_at) returns the minimum created_at date of all variants.\n $dateDiff(date1, date2, unit: \"seconds\" | \"minutes\" | \"hours\" | \"days\") - Returns the difference between two dates in the specified unit. E.g. $dateDiff($.order.created_at, $.order.updated_at, \"days\") returns the number of days between the order created_at and updated_at.\n $now([picture [, timezone]]) - Returns current date and time in ISO 8601 format. E.g. $now() => \"2017-05-15T15:12:59.152Z\"\n $split(str[, separator][, limit]) - Splits string into array\n $join(array[, separator]) - Joins array elements into string\n $match(str, pattern[, limit]) - Returns array of regex matches\n $replace(str, pattern, replacement) - Replaces all occurrences of pattern. E.g. $replace(\"abracadabra\", /a.*?a/, \"*\") returns \"ab*ad*bra\". $replace(\"John Smith\", \"John\", \"Marc\") returns Marc Smith.\n $number(arg) - Converts an argument to a number.\n $min(arr) - Returns minimum number of a number array. E.g. $min($map($.variants.price, $number)) returns the minimum price of all variants.\n $max(arr) - Returns maximum number of a number array. E.g. $max($map($.variants.price, $number)) returns the maximum price of all variants.\n $count(array) - Returns array length\n $sort(array[, function]) - Sorts array\n $distinct(array) - Removes duplicates\n $map(array, function) - Applies function to each element\n $filter(array, function) - Filters array based on predicate\n\n- Error handling:\n - If you get an error like \"is not of a type(s) string/number/object\", try to convert the source field, but also consider that the original field or one of its parent might be null. In this case, add a default value.\n - If the error is something like \"instance is not of a type(s) object\", make sure you REALLY create the target schema with the correct type.\n - If the error is something like \"instance is not of a type(s) array or array/null\". In this case, wrap the source selector in an array to ensure it always returns an array. E.g. \"result\": [$.items]\n - if an object is optional but its fields required, you can add a test and default to {}, but do not set the inner fields to default null.\n\nRemember: The goal is to create valid JSONata expressions that accurately transform the source data structure into the required target structure.";
@@ -0,0 +1,17 @@
1
+ import jsonata from "jsonata";
2
+ import type { Schema } from "jsonschema";
3
+ export interface TransformResult {
4
+ success: boolean;
5
+ data?: unknown;
6
+ error?: string;
7
+ }
8
+ /**
9
+ * Extract JSON Schema from data
10
+ * @param data Any data
11
+ * @returns JSON Schema or null
12
+ */
13
+ export declare function getSchemaFromData(data: unknown): unknown;
14
+ export declare function applyJsonataWithValidation(data: unknown, expr: string, schema: unknown): Promise<TransformResult>;
15
+ export declare function applyJsonata(data: unknown, expr: string): Promise<unknown>;
16
+ export declare function extendJsonata(expr: string): jsonata.Expression;
17
+ export declare function addNullableToOptional(schema: Schema): Schema;
@@ -0,0 +1,14 @@
1
+ import { AIAgent } from "@aigne/core";
2
+ declare const mapper: AIAgent<{
3
+ sourceData: string;
4
+ responseSchema: string;
5
+ sourceSchema?: string | undefined;
6
+ instruction?: string | undefined;
7
+ responseData?: string | undefined;
8
+ feedback?: string | undefined;
9
+ }, {
10
+ jsonata: string;
11
+ confidence: number;
12
+ confidenceReasoning: string;
13
+ }>;
14
+ export default mapper;
@@ -0,0 +1,61 @@
1
+ import { AIAgent, PromptBuilder, UserInputTopic } from "@aigne/core";
2
+ import { z } from "zod";
3
+ import { PROMPT_MAPPING } from "../prompts.js";
4
+ const mapper = AIAgent.from({
5
+ subscribeTopic: [UserInputTopic, "mapping_request"],
6
+ publishTopic: "review_request",
7
+ inputSchema: z.object({
8
+ sourceData: z.string(),
9
+ sourceSchema: z.string().optional(),
10
+ responseSchema: z.string(),
11
+ instruction: z.string().optional(),
12
+ responseData: z.string().optional(),
13
+ feedback: z.string().optional(),
14
+ }),
15
+ outputSchema: z.object({
16
+ jsonata: z.string().describe("JSONata expression"),
17
+ confidence: z.number().describe(`Confidence score for the JSONata expression between 0 and 100.
18
+ Give a low confidence score if there are missing fields in the source data.
19
+ Give a low confidence score if there are multiple options for a field and it is unclear which one to choose.`),
20
+ confidenceReasoning: z.string().describe("Reasoning for the confidence score"),
21
+ }),
22
+ includeInputInOutput: true,
23
+ instructions: PromptBuilder.from({
24
+ messages: [
25
+ {
26
+ role: "assistant",
27
+ content: {
28
+ type: "text",
29
+ text: PROMPT_MAPPING,
30
+ },
31
+ },
32
+ {
33
+ role: "user",
34
+ content: {
35
+ type: "text",
36
+ text: `Given a source data and structure, create a jsonata expression in JSON FORMAT.
37
+ Important: The output should be a jsonata expression creating an object that matches the following schema:
38
+ {{responseSchema}}
39
+
40
+ Pay special attention to the requirements in field descriptions
41
+
42
+ The instruction from the user is: {{instruction}}
43
+
44
+ ------
45
+
46
+ Source Data Structure:
47
+ {{sourceSchema}}
48
+
49
+ Source data Sample:
50
+ {{sourceData}}
51
+
52
+ ------
53
+
54
+ Previous feedback:
55
+ {{feedback}}`,
56
+ },
57
+ },
58
+ ],
59
+ }),
60
+ });
61
+ export default mapper;
@@ -0,0 +1,19 @@
1
+ import { FunctionAgent } from "@aigne/core";
2
+ declare const reviewer: FunctionAgent<{
3
+ sourceData: string;
4
+ responseSchema: string;
5
+ jsonata: string;
6
+ }, {
7
+ success: boolean;
8
+ data: null;
9
+ feedback: string;
10
+ } | {
11
+ success: boolean;
12
+ data: unknown;
13
+ feedback?: undefined;
14
+ } | {
15
+ success: false;
16
+ data: unknown;
17
+ feedback: string | undefined;
18
+ }>;
19
+ export default reviewer;
@@ -0,0 +1,45 @@
1
+ import { FunctionAgent, UserOutputTopic } from "@aigne/core";
2
+ import { z } from "zod";
3
+ import { applyJsonataWithValidation } from "../tools.js";
4
+ const reviewer = FunctionAgent.from({
5
+ name: "check_mapping",
6
+ description: "Check the mapping result",
7
+ subscribeTopic: ["review_request"],
8
+ publishTopic: (output) => (output.success ? UserOutputTopic : "mapping_request"),
9
+ inputSchema: z.object({
10
+ sourceData: z.string(),
11
+ jsonata: z.string(),
12
+ responseSchema: z.string(),
13
+ }),
14
+ process: async ({ sourceData, jsonata, responseSchema, }) => {
15
+ let parsedSourceData = null;
16
+ let parsedResponseSchema = null;
17
+ try {
18
+ parsedSourceData = sourceData ? JSON.parse(sourceData) : null;
19
+ parsedResponseSchema = responseSchema ? JSON.parse(responseSchema) : null;
20
+ }
21
+ catch (parseError) {
22
+ // input data is not valid JSON, return success
23
+ return {
24
+ success: true,
25
+ data: null,
26
+ feedback: `JSON parsing failed: ${parseError.message}`,
27
+ };
28
+ }
29
+ const transformation = await applyJsonataWithValidation(parsedSourceData, jsonata, parsedResponseSchema);
30
+ // if transformation is successful, return success
31
+ if (transformation.success) {
32
+ return {
33
+ success: true,
34
+ data: transformation.data,
35
+ };
36
+ }
37
+ return {
38
+ success: transformation.success,
39
+ data: transformation.data,
40
+ feedback: transformation.error,
41
+ };
42
+ },
43
+ includeInputInOutput: true,
44
+ });
45
+ export default reviewer;
@@ -0,0 +1,18 @@
1
+ import { type ChatModel } from "@aigne/core";
2
+ export interface TransformInput {
3
+ responseSchema: string;
4
+ responseSampleData?: string;
5
+ sourceData?: string;
6
+ sourceSchema?: string;
7
+ instruction?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ export declare function generateMapping({ input, model, }: {
11
+ input: TransformInput;
12
+ model: ChatModel;
13
+ }): Promise<{
14
+ jsonata: string;
15
+ confidence: number;
16
+ confidenceReasoning: string;
17
+ } | null>;
18
+ export { applyJsonata } from "./tools.js";
@@ -0,0 +1,21 @@
1
+ import { AIGNE, UserInputTopic, UserOutputTopic } from "@aigne/core";
2
+ import mapper from "./agents/mapper.js";
3
+ import reviewer from "./agents/reviewer.js";
4
+ import { getSchemaFromData } from "./tools.js";
5
+ export async function generateMapping({ input, model, }) {
6
+ if (!model)
7
+ throw new Error("model is required to run data mapper");
8
+ // if sourceSchema is not provided, generate it from sourceData
9
+ if (!input.sourceSchema && input.sourceData) {
10
+ input.sourceSchema = JSON.stringify(getSchemaFromData(JSON.parse(input.sourceData)));
11
+ }
12
+ const aigne = new AIGNE({ model, agents: [mapper, reviewer] });
13
+ aigne.publish(UserInputTopic, input);
14
+ const { message } = await aigne.subscribe(UserOutputTopic);
15
+ return {
16
+ jsonata: message.jsonata || "",
17
+ confidence: message.confidence || 0,
18
+ confidenceReasoning: message.confidenceReasoning || "",
19
+ };
20
+ }
21
+ export { applyJsonata } from "./tools.js";
@@ -0,0 +1 @@
1
+ export declare const PROMPT_MAPPING = "You are an AI that generates JSONata mapping expressions to transform source data structures into target structures.\n\nGuidelines for creating JSONata mappings:\n\n1. Source References:\n - Use exact field paths from the source data, e.g. $.merchant_category\n - For accessing fields with names containing spaces, use backticks, e.g. $.`merchant category`\n - Jsonata will automatically extract all the fields from the current context. E.g. if you need all variants from all products, you can use $.products.variants. No need to do nested map reduce operations.\n - $. The variable with no name refers to the context value at any point in the input JSON hierarchy. E.g. if the current context is products.price, then $.currency is products.price.currency\n - %. The parent of the current context value. E.g. if the current context is products.variants.size and you want variant name, use %.name\n\n - When multiple source fields could map to a target, use a maximum of 3 fallbacks:\n GOOD: source1 ? source1 : source2 ? source2 : source3 ? source3 : 'default'\n BAD: source1 ? source1 : source1 ? source1 : source1 (repeated fields)\n\n2. Expression Rules:\n - Avoid unnecessary array/string operations\n - Each mapping should be clear and concise\n - Use proper JSONata syntax for coalesce operations\n - Do not use ~> to execute functions. Use the functions directly with the correct arguments or use $map(arr, $function) to apply a function to each element of an array.\n\n3. Array Handling:\n - For mapping to an array of objects, use the following patterns:\n a) When in array scope, use $.{} to map each object:\n Correct: [$.{\"id\": id, \"name\": name}]\n Incorrect: [{\"id\": $.id}]\n b) When outside array scope, include the source path:\n Correct: [$.items.{\"id\": id, \"name\": name}]\n Incorrect: [{\"id\": $.items.id}]\n c) For nested arrays, chain the array operators:\n Correct: [products.variants.{\"size\": size, \"color\": color}]\n Incorrect: [products.[{\"size\": variants.size}]]\n d) You need to use the square brackets [] to map to an array of objects, otherwise it might return an object and fail the validation.\n Correct: variants: [variants.{\"size\": size, \"color\": color}]\n Incorrect: variants: variants.{\"size\": variants.size}\n - For array elements, use JSONata array operators like [0] for first element, [-1] for last element\n - Square bracket notation [] can be used with predicates, e.g. items[type='book']\n\n4. Field Selection Priority:\n - Prefer variant-specific fields over general fields (e.g., sizeVariants.description over sizes)\n - Choose the most specific/detailed field available (e.g., type=\"shelf\" over category=\"furniture\")\n\n5. Filters:\n - Pay special attention to filter statements in the instruction and the schema description. Add them to the generated jsonata expression.\n Example: Get me all products with SKU 0406654608 or products: {\"type\": \"array\", description: \"only products with SKU 0406654608\"}\n Generated jsonata expression: Account.Order.Product[SKU = \"0406654608\"].{\"Description\": Description}\n - For filtering with arrays, you can use the \"in\" operator. E.g. library.books[\"Steven King\" in authors]\n\n6. Data Integrity:\n - ONLY use fields that exist in the source data structure\n - If no matching source field exists, leave the field undefined\n - Never invent or assume the existence of fields not present in the source data\n\n7. Function Calls:\n - You may use the following functions if prompted:\n $string(arg) - Converts argument to string\n $length(str) - Returns string length\n $substring(str, start[, length]) - Extracts substring\n $substringBefore(str, chars) - Gets substring before specified chars\n $substringAfter(str, chars) - Gets substring after specified chars\n $uppercase(str) - Converts to uppercase\n $lowercase(str) - Converts to lowercase\n $trim(str) - Removes whitespace from both ends\n $pad(str, width[, char]) - Pads string to specified width\n $contains(str, substring) - Tests if string contains substring\n $toMillis(timestamp [, picture]) - Converts ISO 8601 timestamp to milliseconds. E.g. $toMillis(\"2017-11-07T15:07:54.972Z\") => 1510067274972\n $toDate(str | number) - Converts any timestamp string to valid ISO 8601 date string. E.g. $toDate(\"Oct 15, 2024 12:00:00 AM UTC\") => \"2024-10-15T00:00:00.000Z\", $toDate(1728873600000) => \"2024-10-15T00:00:00.000Z\"\n $dateMax(arr) - Returns the maximum date of an array of dates. E.g. $dateMax([\"2017-11-07T15:07:54.972Z\", \"Oct 15, 2012 12:00:00 AM UTC\"]) returns \"2017-11-07T15:07:54.972Z\".\n $dateMin(arr) - Returns the minimum date of an array of dates. E.g. $dateMin($.variants.created_at) returns the minimum created_at date of all variants.\n $dateDiff(date1, date2, unit: \"seconds\" | \"minutes\" | \"hours\" | \"days\") - Returns the difference between two dates in the specified unit. E.g. $dateDiff($.order.created_at, $.order.updated_at, \"days\") returns the number of days between the order created_at and updated_at.\n $now([picture [, timezone]]) - Returns current date and time in ISO 8601 format. E.g. $now() => \"2017-05-15T15:12:59.152Z\"\n $split(str[, separator][, limit]) - Splits string into array\n $join(array[, separator]) - Joins array elements into string\n $match(str, pattern[, limit]) - Returns array of regex matches\n $replace(str, pattern, replacement) - Replaces all occurrences of pattern. E.g. $replace(\"abracadabra\", /a.*?a/, \"*\") returns \"ab*ad*bra\". $replace(\"John Smith\", \"John\", \"Marc\") returns Marc Smith.\n $number(arg) - Converts an argument to a number.\n $min(arr) - Returns minimum number of a number array. E.g. $min($map($.variants.price, $number)) returns the minimum price of all variants.\n $max(arr) - Returns maximum number of a number array. E.g. $max($map($.variants.price, $number)) returns the maximum price of all variants.\n $count(array) - Returns array length\n $sort(array[, function]) - Sorts array\n $distinct(array) - Removes duplicates\n $map(array, function) - Applies function to each element\n $filter(array, function) - Filters array based on predicate\n\n- Error handling:\n - If you get an error like \"is not of a type(s) string/number/object\", try to convert the source field, but also consider that the original field or one of its parent might be null. In this case, add a default value.\n - If the error is something like \"instance is not of a type(s) object\", make sure you REALLY create the target schema with the correct type.\n - If the error is something like \"instance is not of a type(s) array or array/null\". In this case, wrap the source selector in an array to ensure it always returns an array. E.g. \"result\": [$.items]\n - if an object is optional but its fields required, you can add a test and default to {}, but do not set the inner fields to default null.\n\nRemember: The goal is to create valid JSONata expressions that accurately transform the source data structure into the required target structure.";
@@ -0,0 +1,91 @@
1
+ export const PROMPT_MAPPING = `You are an AI that generates JSONata mapping expressions to transform source data structures into target structures.
2
+
3
+ Guidelines for creating JSONata mappings:
4
+
5
+ 1. Source References:
6
+ - Use exact field paths from the source data, e.g. $.merchant_category
7
+ - For accessing fields with names containing spaces, use backticks, e.g. $.\`merchant category\`
8
+ - Jsonata will automatically extract all the fields from the current context. E.g. if you need all variants from all products, you can use $.products.variants. No need to do nested map reduce operations.
9
+ - $. The variable with no name refers to the context value at any point in the input JSON hierarchy. E.g. if the current context is products.price, then $.currency is products.price.currency
10
+ - %. The parent of the current context value. E.g. if the current context is products.variants.size and you want variant name, use %.name
11
+
12
+ - When multiple source fields could map to a target, use a maximum of 3 fallbacks:
13
+ GOOD: source1 ? source1 : source2 ? source2 : source3 ? source3 : 'default'
14
+ BAD: source1 ? source1 : source1 ? source1 : source1 (repeated fields)
15
+
16
+ 2. Expression Rules:
17
+ - Avoid unnecessary array/string operations
18
+ - Each mapping should be clear and concise
19
+ - Use proper JSONata syntax for coalesce operations
20
+ - Do not use ~> to execute functions. Use the functions directly with the correct arguments or use $map(arr, $function) to apply a function to each element of an array.
21
+
22
+ 3. Array Handling:
23
+ - For mapping to an array of objects, use the following patterns:
24
+ a) When in array scope, use $.{} to map each object:
25
+ Correct: [$.{"id": id, "name": name}]
26
+ Incorrect: [{"id": $.id}]
27
+ b) When outside array scope, include the source path:
28
+ Correct: [$.items.{"id": id, "name": name}]
29
+ Incorrect: [{"id": $.items.id}]
30
+ c) For nested arrays, chain the array operators:
31
+ Correct: [products.variants.{"size": size, "color": color}]
32
+ Incorrect: [products.[{"size": variants.size}]]
33
+ d) You need to use the square brackets [] to map to an array of objects, otherwise it might return an object and fail the validation.
34
+ Correct: variants: [variants.{"size": size, "color": color}]
35
+ Incorrect: variants: variants.{"size": variants.size}
36
+ - For array elements, use JSONata array operators like [0] for first element, [-1] for last element
37
+ - Square bracket notation [] can be used with predicates, e.g. items[type='book']
38
+
39
+ 4. Field Selection Priority:
40
+ - Prefer variant-specific fields over general fields (e.g., sizeVariants.description over sizes)
41
+ - Choose the most specific/detailed field available (e.g., type="shelf" over category="furniture")
42
+
43
+ 5. Filters:
44
+ - Pay special attention to filter statements in the instruction and the schema description. Add them to the generated jsonata expression.
45
+ Example: Get me all products with SKU 0406654608 or products: {"type": "array", description: "only products with SKU 0406654608"}
46
+ Generated jsonata expression: Account.Order.Product[SKU = "0406654608"].{"Description": Description}
47
+ - For filtering with arrays, you can use the "in" operator. E.g. library.books["Steven King" in authors]
48
+
49
+ 6. Data Integrity:
50
+ - ONLY use fields that exist in the source data structure
51
+ - If no matching source field exists, leave the field undefined
52
+ - Never invent or assume the existence of fields not present in the source data
53
+
54
+ 7. Function Calls:
55
+ - You may use the following functions if prompted:
56
+ $string(arg) - Converts argument to string
57
+ $length(str) - Returns string length
58
+ $substring(str, start[, length]) - Extracts substring
59
+ $substringBefore(str, chars) - Gets substring before specified chars
60
+ $substringAfter(str, chars) - Gets substring after specified chars
61
+ $uppercase(str) - Converts to uppercase
62
+ $lowercase(str) - Converts to lowercase
63
+ $trim(str) - Removes whitespace from both ends
64
+ $pad(str, width[, char]) - Pads string to specified width
65
+ $contains(str, substring) - Tests if string contains substring
66
+ $toMillis(timestamp [, picture]) - Converts ISO 8601 timestamp to milliseconds. E.g. $toMillis("2017-11-07T15:07:54.972Z") => 1510067274972
67
+ $toDate(str | number) - Converts any timestamp string to valid ISO 8601 date string. E.g. $toDate("Oct 15, 2024 12:00:00 AM UTC") => "2024-10-15T00:00:00.000Z", $toDate(1728873600000) => "2024-10-15T00:00:00.000Z"
68
+ $dateMax(arr) - Returns the maximum date of an array of dates. E.g. $dateMax(["2017-11-07T15:07:54.972Z", "Oct 15, 2012 12:00:00 AM UTC"]) returns "2017-11-07T15:07:54.972Z".
69
+ $dateMin(arr) - Returns the minimum date of an array of dates. E.g. $dateMin($.variants.created_at) returns the minimum created_at date of all variants.
70
+ $dateDiff(date1, date2, unit: "seconds" | "minutes" | "hours" | "days") - Returns the difference between two dates in the specified unit. E.g. $dateDiff($.order.created_at, $.order.updated_at, "days") returns the number of days between the order created_at and updated_at.
71
+ $now([picture [, timezone]]) - Returns current date and time in ISO 8601 format. E.g. $now() => "2017-05-15T15:12:59.152Z"
72
+ $split(str[, separator][, limit]) - Splits string into array
73
+ $join(array[, separator]) - Joins array elements into string
74
+ $match(str, pattern[, limit]) - Returns array of regex matches
75
+ $replace(str, pattern, replacement) - Replaces all occurrences of pattern. E.g. $replace("abracadabra", /a.*?a/, "*") returns "ab*ad*bra". $replace("John Smith", "John", "Marc") returns Marc Smith.
76
+ $number(arg) - Converts an argument to a number.
77
+ $min(arr) - Returns minimum number of a number array. E.g. $min($map($.variants.price, $number)) returns the minimum price of all variants.
78
+ $max(arr) - Returns maximum number of a number array. E.g. $max($map($.variants.price, $number)) returns the maximum price of all variants.
79
+ $count(array) - Returns array length
80
+ $sort(array[, function]) - Sorts array
81
+ $distinct(array) - Removes duplicates
82
+ $map(array, function) - Applies function to each element
83
+ $filter(array, function) - Filters array based on predicate
84
+
85
+ - Error handling:
86
+ - If you get an error like "is not of a type(s) string/number/object", try to convert the source field, but also consider that the original field or one of its parent might be null. In this case, add a default value.
87
+ - If the error is something like "instance is not of a type(s) object", make sure you REALLY create the target schema with the correct type.
88
+ - If the error is something like "instance is not of a type(s) array or array/null". In this case, wrap the source selector in an array to ensure it always returns an array. E.g. "result": [$.items]
89
+ - if an object is optional but its fields required, you can add a test and default to {}, but do not set the inner fields to default null.
90
+
91
+ Remember: The goal is to create valid JSONata expressions that accurately transform the source data structure into the required target structure.`;
@@ -0,0 +1,17 @@
1
+ import jsonata from "jsonata";
2
+ import type { Schema } from "jsonschema";
3
+ export interface TransformResult {
4
+ success: boolean;
5
+ data?: unknown;
6
+ error?: string;
7
+ }
8
+ /**
9
+ * Extract JSON Schema from data
10
+ * @param data Any data
11
+ * @returns JSON Schema or null
12
+ */
13
+ export declare function getSchemaFromData(data: unknown): unknown;
14
+ export declare function applyJsonataWithValidation(data: unknown, expr: string, schema: unknown): Promise<TransformResult>;
15
+ export declare function applyJsonata(data: unknown, expr: string): Promise<unknown>;
16
+ export declare function extendJsonata(expr: string): jsonata.Expression;
17
+ export declare function addNullableToOptional(schema: Schema): Schema;
@@ -0,0 +1,167 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import jsonata from "jsonata";
3
+ import { Validator } from "jsonschema";
4
+ import toJsonSchema from "to-json-schema";
5
+ /**
6
+ * Extract JSON Schema from data
7
+ * @param data Any data
8
+ * @returns JSON Schema or null
9
+ */
10
+ export function getSchemaFromData(data) {
11
+ if (!data)
12
+ return null;
13
+ return toJsonSchema(data);
14
+ }
15
+ export async function applyJsonataWithValidation(data, expr, schema) {
16
+ try {
17
+ const result = await applyJsonata(data, expr);
18
+ if (result === null ||
19
+ result === undefined ||
20
+ (Array.isArray(result) && result.length === 0) ||
21
+ (typeof result === "object" && Object.keys(result).length === 0)) {
22
+ return { success: false, error: "Result is empty" };
23
+ }
24
+ const validator = new Validator();
25
+ const optionalSchema = addNullableToOptional(schema);
26
+ const validation = validator.validate(result, optionalSchema);
27
+ if (!validation.valid) {
28
+ return {
29
+ success: false,
30
+ error: validation.errors
31
+ .map((e) => `${e.stack}. Source: ${e.instance ? JSON.stringify(e.instance) : "undefined"}`)
32
+ .join("\n")
33
+ .slice(0, 5000),
34
+ };
35
+ }
36
+ return { success: true, data: result };
37
+ }
38
+ catch (error) {
39
+ return { success: false, error: `Validation failed: ${error.message}` };
40
+ }
41
+ }
42
+ export async function applyJsonata(data, expr) {
43
+ try {
44
+ const expression = extendJsonata(expr);
45
+ const result = await expression.evaluate(data);
46
+ return result;
47
+ }
48
+ catch (error) {
49
+ throw new Error(`JSONata evaluation failed for expression "${expr}": ${error.message}`);
50
+ }
51
+ }
52
+ export function extendJsonata(expr) {
53
+ const expression = jsonata(expr);
54
+ expression.registerFunction("max", (arr) => {
55
+ if (Array.isArray(arr)) {
56
+ return Math.max(...arr.map(Number).filter((n) => !Number.isNaN(n)));
57
+ }
58
+ return arr;
59
+ });
60
+ expression.registerFunction("min", (arr) => {
61
+ if (Array.isArray(arr)) {
62
+ return Math.min(...arr.map(Number).filter((n) => !Number.isNaN(n)));
63
+ }
64
+ return arr;
65
+ });
66
+ expression.registerFunction("number", (value) => Number.parseFloat(value));
67
+ expression.registerFunction("substring", (str, start, end) => String(str).substring(start, end));
68
+ expression.registerFunction("replace", (obj, pattern, replacement) => {
69
+ if (Array.isArray(obj)) {
70
+ return obj.map((item) => String(item).replace(pattern, replacement));
71
+ }
72
+ if (typeof obj === "object") {
73
+ return Object.fromEntries(Object.entries(obj || {}).map(([key, value]) => [
74
+ key,
75
+ String(value).replace(pattern, replacement),
76
+ ]));
77
+ }
78
+ return String(obj).replace(pattern, replacement);
79
+ });
80
+ expression.registerFunction("toDate", (date) => {
81
+ try {
82
+ // Handle numeric timestamps (milliseconds or seconds)
83
+ if (typeof date === "number" || /^\d+$/.test(date)) {
84
+ const timestamp = typeof date === "number" ? date : Number.parseInt(date, 10);
85
+ // If timestamp is in seconds (typically 10 digits), convert to milliseconds
86
+ const millisTimestamp = timestamp < 10000000000 ? timestamp * 1000 : timestamp;
87
+ return new Date(millisTimestamp).toISOString();
88
+ }
89
+ // Handle date strings in MM/DD/YYYY format
90
+ const match = String(date).match(/^(\d{2})\/(\d{2})\/(\d{4})(?:\s+(\d{2}):(\d{2}):(\d{2}))?$/);
91
+ if (match) {
92
+ // eslint-disable-next-line @typescript-eslint/naming-convention
93
+ const [_, month, day, year, hours = "00", minutes = "00", seconds = "00"] = match;
94
+ const isoDate = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.000Z`;
95
+ return new Date(isoDate).toISOString();
96
+ }
97
+ // Default case: try standard Date parsing
98
+ return new Date(date).toISOString();
99
+ }
100
+ catch (e) {
101
+ throw new Error(`Invalid date: ${e.message}`);
102
+ }
103
+ });
104
+ expression.registerFunction("dateMax", (dates) => dates.reduce((max, curr) => (new Date(max) > new Date(curr) ? max : curr)));
105
+ expression.registerFunction("dateMin", (dates) => dates.reduce((min, curr) => (new Date(min) < new Date(curr) ? min : curr)));
106
+ expression.registerFunction("dateDiff", (date1, date2, unit = "days") => {
107
+ const d1 = new Date(date1);
108
+ const d2 = new Date(date2);
109
+ const diff = Math.abs(d1.getTime() - d2.getTime());
110
+ switch (unit.toLowerCase()) {
111
+ case "seconds":
112
+ return Math.floor(diff / 1000);
113
+ case "minutes":
114
+ return Math.floor(diff / (1000 * 60));
115
+ case "hours":
116
+ return Math.floor(diff / (1000 * 60 * 60));
117
+ case "days":
118
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
119
+ default:
120
+ return diff; // milliseconds
121
+ }
122
+ });
123
+ return expression;
124
+ }
125
+ export function addNullableToOptional(schema) {
126
+ if (!schema || typeof schema !== "object")
127
+ return schema;
128
+ const newSchema = { ...schema };
129
+ if (schema.type === "object" && schema.properties) {
130
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
131
+ newSchema.properties = Object.entries(schema.properties).reduce((acc, [key, value]) => ({
132
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
133
+ ...acc,
134
+ [key]: !required.has(key) ? makeNullable(value) : addNullableToOptional(value),
135
+ }), {});
136
+ }
137
+ if (schema.type === "array" && schema.items) {
138
+ newSchema.items = addNullableToOptional(schema.items);
139
+ }
140
+ return newSchema;
141
+ }
142
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
143
+ function makeNullable(schema) {
144
+ if (!schema || typeof schema !== "object")
145
+ return schema;
146
+ const newSchema = { ...schema };
147
+ if (Array.isArray(schema.type)) {
148
+ if (!schema.type.includes("null")) {
149
+ newSchema.type = [...schema.type, "null"];
150
+ }
151
+ }
152
+ else if (schema.type) {
153
+ newSchema.type = [schema.type, "null"];
154
+ }
155
+ // Recursively process nested properties
156
+ if (schema.properties) {
157
+ newSchema.properties = Object.entries(schema.properties).reduce((acc, [key, value]) => ({
158
+ // biome-ignore lint/performance/noAccumulatingSpread: <explanation>
159
+ ...acc,
160
+ [key]: makeNullable(value),
161
+ }), {});
162
+ }
163
+ if (schema.items) {
164
+ newSchema.items = makeNullable(schema.items);
165
+ }
166
+ return newSchema;
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/agent-library",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Collection of agent libraries for AIGNE framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -39,12 +39,17 @@
39
39
  }
40
40
  },
41
41
  "dependencies": {
42
+ "jsonata": "^2.0.6",
43
+ "jsonschema": "^1.5.0",
44
+ "to-json-schema": "^0.2.5",
42
45
  "fastq": "^1.19.1",
43
46
  "yaml": "^2.7.1",
44
47
  "zod": "^3.24.4",
45
- "@aigne/core": "^1.17.0"
48
+ "@aigne/core": "^1.17.0",
49
+ "@aigne/openai": "^0.2.0"
46
50
  },
47
51
  "devDependencies": {
52
+ "@types/to-json-schema": "^0.2.4",
48
53
  "@types/bun": "^1.2.12",
49
54
  "npm-run-all": "^4.1.5",
50
55
  "rimraf": "^6.0.1",