WIP - using deep agent to create complex workflow
This commit is contained in:
136
backend/package-lock.json
generated
136
backend/package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.7.5",
|
||||
"@fastify/websocket": "^10.0.1",
|
||||
"@langchain/core": "^1.1.12",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph": "^1.0.15",
|
||||
"@langchain/openai": "^1.2.1",
|
||||
"@nestjs/bullmq": "^10.1.0",
|
||||
@@ -29,9 +29,10 @@
|
||||
"bullmq": "^5.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"deepagents": "^1.5.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"knex": "^3.1.0",
|
||||
"langchain": "^1.2.7",
|
||||
"langchain": "^1.2.10",
|
||||
"mysql2": "^3.15.3",
|
||||
"objection": "^3.1.5",
|
||||
"openai": "^6.15.0",
|
||||
@@ -228,6 +229,26 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.71.2",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz",
|
||||
"integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -689,6 +710,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -1689,10 +1719,26 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/anthropic": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.10.tgz",
|
||||
"integrity": "sha512-VXq5fsEJ4FB5XGrnoG+bfm0I7OlmYLI4jZ6cX9RasyqhGo9wcDyKw1+uEQ1H7Og7jWrTa1bfXCun76wttewJnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.0",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "1.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.12.tgz",
|
||||
"integrity": "sha512-sHWLvhyLi3fntlg3MEPB89kCjxEX7/+imlIYJcp6uFGCAZfGxVWklqp22HwjT1szorUBYrkO8u0YA554ReKxGQ==",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.15.tgz",
|
||||
"integrity": "sha512-b8RN5DkWAmDAlMu/UpTZEluYwCLpm63PPWniRKlE8ie3KkkE7IuMQ38pf4kV1iaiI+d99BEQa2vafQHfCujsRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
@@ -2487,7 +2533,6 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -2501,7 +2546,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -2511,7 +2555,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -4029,7 +4072,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -4754,6 +4796,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepagents": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/deepagents/-/deepagents-1.5.0.tgz",
|
||||
"integrity": "sha512-tjZLOISPMpqfk+k/iE1uIZavXW9j4NrhopUmH5ARqzmk95EEtGDyN++tgnY+tdVOOZTjE2LHjOVV7or58dtx8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^1.3.7",
|
||||
"@langchain/core": "^1.1.12",
|
||||
"@langchain/langgraph": "^1.0.14",
|
||||
"fast-glob": "^3.3.3",
|
||||
"langchain": "^1.2.7",
|
||||
"micromatch": "^4.0.8",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -5514,7 +5572,6 @@
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -5740,7 +5797,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -6145,7 +6201,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -6594,7 +6649,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6623,7 +6677,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -6658,7 +6711,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -7609,6 +7661,19 @@
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -7811,9 +7876,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/langchain": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.7.tgz",
|
||||
"integrity": "sha512-G+3Ftz/08CurJaE7LukQGBf3mCSz7XM8LZeAaFPg391Ru4lT8eLYfG6Fv4ZI0u6EBsPVcOQfaS9ig8nCRmJeqA==",
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.10.tgz",
|
||||
"integrity": "sha512-9uVxOJE/RTECvNutQfOLwH7f6R9mcq0G/IMHwA2eptDA86R/Yz2zWMz4vARVFPxPrdSJ9nJFDPAqRQlRFwdHBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph": "^1.0.0",
|
||||
@@ -7826,7 +7891,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "1.1.12"
|
||||
"@langchain/core": "1.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/langchain/node_modules/uuid": {
|
||||
@@ -8201,7 +8266,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -8211,7 +8275,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -8225,7 +8288,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -9388,7 +9450,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -9727,7 +9788,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -10657,7 +10717,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -10709,6 +10768,12 @@
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||
@@ -11455,6 +11520,21 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
@@ -11508,9 +11588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.7.5",
|
||||
"@fastify/websocket": "^10.0.1",
|
||||
"@langchain/core": "^1.1.12",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph": "^1.0.15",
|
||||
"@langchain/openai": "^1.2.1",
|
||||
"@nestjs/bullmq": "^10.1.0",
|
||||
@@ -46,9 +46,10 @@
|
||||
"bullmq": "^5.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"deepagents": "^1.5.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"knex": "^3.1.0",
|
||||
"langchain": "^1.2.7",
|
||||
"langchain": "^1.2.10",
|
||||
"mysql2": "^3.15.3",
|
||||
"objection": "^3.1.5",
|
||||
"openai": "^6.15.0",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { JsonOutputParser } from '@langchain/core/output_parsers';
|
||||
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { createDeepAgent } from 'deepagents';
|
||||
import { ObjectService } from '../object/object.service';
|
||||
import { PageLayoutService } from '../page-layout/page-layout.service';
|
||||
import { TenantDatabaseService } from '../tenant/tenant-database.service';
|
||||
@@ -68,32 +69,275 @@ export class AiAssistantService {
|
||||
const prior = this.conversationState.get(conversationKey);
|
||||
|
||||
const trimmedHistory = Array.isArray(history) ? history.slice(-6) : [];
|
||||
const initialState: AiAssistantState = {
|
||||
message: this.combineHistory(trimmedHistory, message),
|
||||
history: trimmedHistory,
|
||||
context: context || {},
|
||||
extractedFields: prior?.fields,
|
||||
};
|
||||
|
||||
// Use Deep Agent as the main coordinator
|
||||
const result = await this.runDeepAgent(tenantId, userId, message, trimmedHistory, context, prior);
|
||||
|
||||
const finalState = await this.runAssistantGraph(tenantId, userId, initialState);
|
||||
|
||||
if (finalState.record) {
|
||||
// Update conversation state based on result
|
||||
if (result.record) {
|
||||
this.conversationState.delete(conversationKey);
|
||||
} else if (finalState.extractedFields && Object.keys(finalState.extractedFields).length > 0) {
|
||||
} else if ('extractedFields' in result && result.extractedFields && Object.keys(result.extractedFields).length > 0) {
|
||||
this.conversationState.set(conversationKey, {
|
||||
fields: finalState.extractedFields,
|
||||
fields: result.extractedFields,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
reply: finalState.reply || 'How can I help?',
|
||||
action: finalState.action,
|
||||
missingFields: finalState.missingFields,
|
||||
record: finalState.record,
|
||||
reply: result.reply || 'How can I help?',
|
||||
action: result.action,
|
||||
missingFields: result.missingFields,
|
||||
record: result.record,
|
||||
};
|
||||
}
|
||||
|
||||
private async runDeepAgent(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
message: string,
|
||||
history: AiAssistantState['history'],
|
||||
context: AiAssistantState['context'],
|
||||
prior?: { fields: Record<string, any>; updatedAt: number },
|
||||
): Promise<AiAssistantReply & { extractedFields?: Record<string, any> }> {
|
||||
const openAiConfig = await this.getOpenAiConfig(tenantId);
|
||||
if (!openAiConfig) {
|
||||
this.logger.warn('No OpenAI config found; using fallback graph execution.');
|
||||
// Fallback to direct graph execution if no OpenAI config
|
||||
const initialState: AiAssistantState = {
|
||||
message: this.combineHistory(history, message),
|
||||
history: history,
|
||||
context: context || {},
|
||||
extractedFields: prior?.fields,
|
||||
};
|
||||
const graph = this.buildResolveOrCreateRecordGraph(tenantId, userId);
|
||||
const result = await graph.invoke(initialState);
|
||||
return {
|
||||
reply: result.reply || 'How can I help?',
|
||||
action: result.action,
|
||||
missingFields: result.missingFields,
|
||||
record: result.record,
|
||||
};
|
||||
}
|
||||
|
||||
// Build the compiled subagent
|
||||
const compiledSubagent = this.buildResolveOrCreateRecordGraph(tenantId, userId);
|
||||
|
||||
// Create Deep Agent with the subagent
|
||||
const mainModel = new ChatOpenAI({
|
||||
apiKey: openAiConfig.apiKey,
|
||||
model: this.normalizeChatModel(openAiConfig.model),
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const systemPrompt = this.buildDeepAgentSystemPrompt(context);
|
||||
|
||||
const agent = createDeepAgent({
|
||||
model: mainModel,
|
||||
systemPrompt,
|
||||
tools: [],
|
||||
subagents: [
|
||||
{
|
||||
name: 'resolve-or-create-record',
|
||||
description: [
|
||||
'ALWAYS use this subagent for ANY operation involving CRM records (create, find, or lookup).',
|
||||
'',
|
||||
'This subagent handles:',
|
||||
'- Looking up existing records by name or other fields',
|
||||
'- Creating new records with the provided field values',
|
||||
'- Resolving related records (e.g., finding an Account to link a Contact to)',
|
||||
'- Validating required fields before creating records',
|
||||
'',
|
||||
'IMPORTANT: When invoking this subagent, include in your message:',
|
||||
'- The exact user request',
|
||||
'- The object type (Account, Contact, etc.) if known from context',
|
||||
'- Any previously collected information',
|
||||
'',
|
||||
'Example invocations:',
|
||||
'- "Create Account named Acme Corp" (objectApiName: Account)',
|
||||
'- "Create Contact John Doe" (objectApiName: Contact)',
|
||||
'- "Add phone number 555-1234 for Contact John" (objectApiName: ContactDetail)',
|
||||
].join('\n'),
|
||||
runnable: compiledSubagent,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Convert history to messages format
|
||||
const messages: BaseMessage[] = [];
|
||||
if (history && history.length > 0) {
|
||||
for (const entry of history) {
|
||||
if (entry.role === 'user') {
|
||||
messages.push(new HumanMessage(entry.text));
|
||||
} else if (entry.role === 'assistant') {
|
||||
messages.push(new AIMessage(entry.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
messages.push(new HumanMessage(message));
|
||||
|
||||
// Include context information in the first message if available
|
||||
let contextInfo = '';
|
||||
if (context?.objectApiName) {
|
||||
contextInfo += `\n[System Context: User is working with ${context.objectApiName} object`;
|
||||
if (context.recordId) {
|
||||
contextInfo += `, record ID: ${context.recordId}`;
|
||||
}
|
||||
contextInfo += ']';
|
||||
}
|
||||
if (prior?.fields && Object.keys(prior.fields).length > 0) {
|
||||
contextInfo += `\n[Previously collected field values: ${JSON.stringify(prior.fields)}]`;
|
||||
}
|
||||
|
||||
if (contextInfo && messages.length > 0) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage instanceof HumanMessage) {
|
||||
messages[messages.length - 1] = new HumanMessage(
|
||||
lastMessage.content + contextInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('=== DEEP AGENT: Starting invocation ===');
|
||||
console.log('Messages:', messages.map(m => ({ role: m._getType(), content: m.content })));
|
||||
|
||||
const result = await agent.invoke({ messages });
|
||||
|
||||
console.log('=== DEEP AGENT: Result received ===');
|
||||
console.log('Result messages count:', result.messages.length);
|
||||
|
||||
// Look for subagent results in the messages
|
||||
let subagentResult: any = null;
|
||||
for (let i = result.messages.length - 1; i >= 0; i--) {
|
||||
const msg = result.messages[i];
|
||||
console.log(`Message ${i}:`, {
|
||||
type: msg._getType(),
|
||||
content: typeof msg.content === 'string' ? msg.content.substring(0, 200) : msg.content,
|
||||
additional_kwargs: msg.additional_kwargs,
|
||||
});
|
||||
|
||||
// Check if this message has subagent output data
|
||||
if (msg.additional_kwargs?.action || msg.additional_kwargs?.record) {
|
||||
subagentResult = msg.additional_kwargs;
|
||||
console.log('Found subagent result in message additional_kwargs:', subagentResult);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const lastMsg = result.messages[result.messages.length - 1];
|
||||
const replyText = typeof lastMsg.content === 'string'
|
||||
? lastMsg.content
|
||||
: 'How can I help?';
|
||||
|
||||
console.log('Final reply text:', replyText);
|
||||
|
||||
// If we found subagent results, use them; otherwise use defaults
|
||||
if (subagentResult) {
|
||||
console.log('=== DEEP AGENT: Using subagent result ===');
|
||||
return {
|
||||
reply: replyText,
|
||||
action: subagentResult.action || 'clarify',
|
||||
missingFields: subagentResult.missingFields || [],
|
||||
record: subagentResult.record,
|
||||
extractedFields: subagentResult.extractedFields,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('=== DEEP AGENT: No subagent result found, using defaults ===');
|
||||
return {
|
||||
reply: replyText,
|
||||
action: 'clarify',
|
||||
missingFields: [],
|
||||
record: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Deep Agent execution failed: ${error.message}`, error.stack);
|
||||
// Fallback to direct graph execution
|
||||
const initialState: AiAssistantState = {
|
||||
message: this.combineHistory(history, message),
|
||||
history: history,
|
||||
context: context || {},
|
||||
extractedFields: prior?.fields,
|
||||
};
|
||||
const graph = this.buildResolveOrCreateRecordGraph(tenantId, userId);
|
||||
const result = await graph.invoke(initialState);
|
||||
return {
|
||||
reply: result.reply || 'How can I help?',
|
||||
action: result.action,
|
||||
missingFields: result.missingFields,
|
||||
record: result.record,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private buildDeepAgentSystemPrompt(context?: AiAssistantState['context']): string {
|
||||
const contextInfo = context?.objectApiName
|
||||
? ` The user is currently working with the ${context.objectApiName} object.`
|
||||
: '';
|
||||
|
||||
return [
|
||||
'You are an AI assistant helping users interact with a CRM system through natural conversation.',
|
||||
'Your role is to understand user requests and coordinate with specialized subagents to fulfill them.',
|
||||
'',
|
||||
'Core Responsibilities:',
|
||||
'- Parse user requests to understand what records they want to create or find',
|
||||
'- Identify the record type (Account, Contact, ContactDetail, etc.)',
|
||||
'- Break down complex multi-step requests into manageable tasks',
|
||||
'- ALWAYS delegate to the "resolve-or-create-record" subagent for ANY record operation',
|
||||
'',
|
||||
'Understanding Record Relationships:',
|
||||
'- When user says "Create X under Y account":',
|
||||
' 1. X is NOT an Account - it is a Contact or child record',
|
||||
' 2. Y is the parent Account name',
|
||||
' 3. You must: (a) First ensure Y account exists, (b) Then create X as a Contact under Y',
|
||||
'- Accounts are top-level organizations/companies',
|
||||
'- Contacts are people/entities that belong to Accounts',
|
||||
'- ContactDetails are phone/email/address records for Contacts or Accounts',
|
||||
'',
|
||||
'Multi-Step Example:',
|
||||
'- User: "Create Chipi under Jeannete Staley account"',
|
||||
' Step 1: Invoke subagent to find/create Account "Jeannete Staley" (objectApiName: Account)',
|
||||
' Step 2: Once account exists, invoke subagent to create Contact "Chipi" linked to that Account (objectApiName: Contact)',
|
||||
'',
|
||||
'When invoking the subagent:',
|
||||
'- Include the record type in parentheses: "Find Account Jeannete Staley (objectApiName: Account)"',
|
||||
'- For child records: "Create Contact Chipi for Account <id> (objectApiName: Contact)"',
|
||||
'- Pass the user\'s full request with context',
|
||||
'',
|
||||
'DO NOT try to create records yourself - ALWAYS use the subagent for:',
|
||||
' * Finding existing records',
|
||||
' * Creating new records',
|
||||
' * Resolving related records',
|
||||
'',
|
||||
'Important Patterns:',
|
||||
'- When a user says "Create X under/for Y", this means:',
|
||||
' 1. You need to first find or verify record Y exists',
|
||||
' 2. Then create record X with a reference to Y',
|
||||
' Example: "Create Max under John Doe Account" means find the Account named "John Doe",',
|
||||
' then create a record named "Max" that references that Account.',
|
||||
'',
|
||||
'- For polymorphic relationships (records that can reference multiple types):',
|
||||
' * ContactDetail records can reference either Account or Contact',
|
||||
' * Infer the correct type from context clues in the user\'s message',
|
||||
'',
|
||||
'CRITICAL - Checking Results:',
|
||||
'- After the subagent responds, CHECK if it actually completed the action',
|
||||
'- Look for phrases like "I still need" or "missing fields" which indicate incomplete work',
|
||||
'- If the subagent asks for more information, relay that to the user - DO NOT claim success',
|
||||
'- Only report success if the subagent explicitly confirms record creation',
|
||||
'- DO NOT fabricate success messages if the subagent indicates it needs more data',
|
||||
'',
|
||||
'Response Style:',
|
||||
'- Be conversational and helpful',
|
||||
'- Confirm what you\'re doing: "I\'ll create Max as a Contact under the John Doe Account"',
|
||||
'- Ask for clarification when the request is ambiguous',
|
||||
'- Report success clearly ONLY when confirmed: "Created Contact Max under John Doe Account"',
|
||||
'- If the subagent needs more info, ask the user for that specific information',
|
||||
'',
|
||||
contextInfo,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async searchRecords(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
@@ -196,13 +440,13 @@ export class AiAssistantService {
|
||||
};
|
||||
}
|
||||
|
||||
private async runAssistantGraph(
|
||||
private buildResolveOrCreateRecordGraph(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
state: AiAssistantState,
|
||||
): Promise<AiAssistantState> {
|
||||
) {
|
||||
const AssistantState = Annotation.Root({
|
||||
message: Annotation<string>(),
|
||||
messages: Annotation<BaseMessage[]>(),
|
||||
history: Annotation<AiAssistantState['history']>(),
|
||||
context: Annotation<AiAssistantState['context']>(),
|
||||
objectDefinition: Annotation<any>(),
|
||||
@@ -215,33 +459,174 @@ export class AiAssistantService {
|
||||
reply: Annotation<string>(),
|
||||
});
|
||||
|
||||
// Entry node to transform Deep Agent messages into our state format
|
||||
const transformInput = async (state: any): Promise<AiAssistantState> => {
|
||||
console.log('=== SUBAGENT: Transform Input ===');
|
||||
console.log('Received state keys:', Object.keys(state));
|
||||
console.log('Has messages:', state.messages ? state.messages.length : 'no');
|
||||
|
||||
// If invoked by Deep Agent, state will have messages array
|
||||
if (state.messages && Array.isArray(state.messages)) {
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
const messageText = typeof lastMessage.content === 'string'
|
||||
? lastMessage.content
|
||||
: '';
|
||||
|
||||
console.log('Extracted message from Deep Agent:', messageText);
|
||||
|
||||
// Try to extract context from message (Deep Agent should include it)
|
||||
const contextMatch = messageText.match(/\[System Context: User is working with (\w+) object(?:, record ID: ([^\]]+))?\]/);
|
||||
const priorFieldsMatch = messageText.match(/\[Previously collected field values: ({[^\]]+})\]/);
|
||||
|
||||
let extractedContext: AiAssistantState['context'] = {};
|
||||
let extractedFields: Record<string, any> | undefined;
|
||||
|
||||
if (contextMatch) {
|
||||
extractedContext.objectApiName = contextMatch[1];
|
||||
if (contextMatch[2]) {
|
||||
extractedContext.recordId = contextMatch[2];
|
||||
}
|
||||
console.log('Extracted context from annotations:', extractedContext);
|
||||
} else {
|
||||
// Fallback: Try to infer object type from the message itself
|
||||
console.log('No context annotation found, attempting to infer from message...');
|
||||
|
||||
// Check for explicit objectApiName mentions in parentheses
|
||||
const explicitMatch = messageText.match(/\(objectApiName:\s*(\w+)\)/);
|
||||
if (explicitMatch) {
|
||||
extractedContext.objectApiName = explicitMatch[1];
|
||||
console.log('Found explicit objectApiName:', extractedContext.objectApiName);
|
||||
} else {
|
||||
// Try to infer from keywords and patterns
|
||||
const lowerMsg = messageText.toLowerCase();
|
||||
|
||||
// Pattern: "Create X under/for Y account" - X is NOT an account, it's a child record
|
||||
const underAccountMatch = messageText.match(/create\s+([\w\s]+?)\s+(?:under|for)\s+([\w\s]+?)\s+account/i);
|
||||
if (underAccountMatch) {
|
||||
// The thing being created is likely a Contact or ContactDetail
|
||||
console.log('Detected "under account" pattern - inferring child record type');
|
||||
|
||||
// Check if it's a contact detail (phone/email)
|
||||
if (lowerMsg.includes('phone') || lowerMsg.includes('email') || lowerMsg.includes('address')) {
|
||||
extractedContext.objectApiName = 'ContactDetail';
|
||||
} else {
|
||||
// Default to Contact for things created under accounts
|
||||
extractedContext.objectApiName = 'Contact';
|
||||
}
|
||||
console.log('Inferred child object type:', extractedContext.objectApiName);
|
||||
} else if (lowerMsg.includes('account') && (lowerMsg.includes('create') || lowerMsg.includes('add'))) {
|
||||
extractedContext.objectApiName = 'Account';
|
||||
} else if (lowerMsg.includes('contact') && !lowerMsg.includes('contact detail')) {
|
||||
extractedContext.objectApiName = 'Contact';
|
||||
} else if (lowerMsg.includes('contact detail') || lowerMsg.includes('contactdetail')) {
|
||||
extractedContext.objectApiName = 'ContactDetail';
|
||||
} else if (lowerMsg.includes('phone') || lowerMsg.includes('email')) {
|
||||
extractedContext.objectApiName = 'ContactDetail';
|
||||
}
|
||||
|
||||
if (extractedContext.objectApiName) {
|
||||
console.log('Inferred objectApiName from keywords:', extractedContext.objectApiName);
|
||||
} else {
|
||||
console.warn('Could not infer objectApiName from message!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (priorFieldsMatch) {
|
||||
try {
|
||||
extractedFields = JSON.parse(priorFieldsMatch[1]);
|
||||
console.log('Extracted prior fields:', extractedFields);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse prior fields');
|
||||
}
|
||||
}
|
||||
|
||||
// Clean the message text from system context annotations
|
||||
const cleanMessage = messageText
|
||||
.replace(/\[System Context:[^\]]+\]/g, '')
|
||||
.replace(/\[Previously collected field values:[^\]]+\]/g, '')
|
||||
.replace(/\(objectApiName:\s*\w+\)/g, '')
|
||||
.trim();
|
||||
|
||||
console.log('Final transformed state:', {
|
||||
message: cleanMessage,
|
||||
context: extractedContext,
|
||||
hasExtractedFields: !!extractedFields,
|
||||
});
|
||||
|
||||
return {
|
||||
message: cleanMessage,
|
||||
messages: state.messages,
|
||||
history: [],
|
||||
context: extractedContext,
|
||||
extractedFields,
|
||||
} as AiAssistantState;
|
||||
}
|
||||
|
||||
// If invoked directly (fallback or testing), use the state as-is
|
||||
console.log('Using direct state (not from Deep Agent)');
|
||||
return state as AiAssistantState;
|
||||
};
|
||||
|
||||
const workflow = new StateGraph(AssistantState)
|
||||
.addNode('transformInput', transformInput)
|
||||
.addNode('loadContext', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Load Context ===');
|
||||
return this.loadContext(tenantId, current);
|
||||
})
|
||||
.addNode('extractFields', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Extract Fields ===');
|
||||
return this.extractFields(tenantId, current);
|
||||
})
|
||||
.addNode('decideNext', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Decide Next ===');
|
||||
return this.decideNextStep(current);
|
||||
})
|
||||
.addNode('createRecord', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Create Record ===');
|
||||
return this.createRecord(tenantId, userId, current);
|
||||
})
|
||||
.addNode('respondMissing', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Respond Missing ===');
|
||||
return this.respondWithMissingFields(current);
|
||||
})
|
||||
.addEdge(START, 'loadContext')
|
||||
.addNode('formatOutput', async (current: AiAssistantState) => {
|
||||
console.log('=== SUBAGENT: Format Output ===');
|
||||
console.log('Final state before output:', {
|
||||
action: current.action,
|
||||
record: current.record,
|
||||
reply: current.reply,
|
||||
missingFields: current.missingFields,
|
||||
});
|
||||
|
||||
// Format the output for Deep Agent to understand
|
||||
const outputMessage = new AIMessage({
|
||||
content: current.reply || 'Completed.',
|
||||
additional_kwargs: {
|
||||
action: current.action,
|
||||
record: current.record,
|
||||
missingFields: current.missingFields,
|
||||
extractedFields: current.extractedFields,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...current,
|
||||
messages: [...(current.messages || []), outputMessage],
|
||||
} as AiAssistantState;
|
||||
})
|
||||
.addEdge(START, 'transformInput')
|
||||
.addEdge('transformInput', 'loadContext')
|
||||
.addEdge('loadContext', 'extractFields')
|
||||
.addEdge('extractFields', 'decideNext')
|
||||
.addConditionalEdges('decideNext', (current: AiAssistantState) => {
|
||||
return current.action === 'create_record' ? 'createRecord' : 'respondMissing';
|
||||
})
|
||||
.addEdge('createRecord', END)
|
||||
.addEdge('respondMissing', END);
|
||||
.addEdge('createRecord', 'formatOutput')
|
||||
.addEdge('respondMissing', 'formatOutput')
|
||||
.addEdge('formatOutput', END);
|
||||
|
||||
const graph = workflow.compile();
|
||||
return graph.invoke(state);
|
||||
return workflow.compile();
|
||||
}
|
||||
|
||||
private async loadContext(
|
||||
@@ -451,6 +836,8 @@ export class AiAssistantService {
|
||||
userId,
|
||||
);
|
||||
|
||||
console.log('record',record);
|
||||
|
||||
const nameValue = enrichedState.extractedFields.name || record?.name || record?.id;
|
||||
const label = enrichedState.objectDefinition.label || enrichedState.objectDefinition.apiName;
|
||||
|
||||
@@ -619,6 +1006,26 @@ export class AiAssistantService {
|
||||
const phoneField = fieldDefinitions.find((field) =>
|
||||
field.apiName.toLowerCase().includes('phone') || field.label.toLowerCase().includes('phone'),
|
||||
);
|
||||
|
||||
// Check for Account lookup field (for Contacts)
|
||||
const accountField = fieldDefinitions.find((field) =>
|
||||
field.apiName === 'accountId' || field.apiName.toLowerCase().includes('account'),
|
||||
);
|
||||
|
||||
// Pattern: "Create X under/for Y account" - extract name and account reference
|
||||
const underAccountMatch = message.match(/create\s+([^\s]+(?:\s+[^\s]+)?)\s+(?:under|for)\s+(.+?)\s+account/i);
|
||||
if (underAccountMatch && nameField) {
|
||||
const recordName = underAccountMatch[1].trim();
|
||||
const accountName = underAccountMatch[2].trim();
|
||||
|
||||
extracted[nameField.apiName] = recordName;
|
||||
|
||||
if (accountField) {
|
||||
// Store the account name for lookup
|
||||
extracted[accountField.apiName] = accountName;
|
||||
console.log('Extracted hierarchical pattern:', { name: recordName, account: accountName });
|
||||
}
|
||||
}
|
||||
|
||||
// Generic pattern matching for any field: "label: value" or "set label to value"
|
||||
for (const field of fieldDefinitions) {
|
||||
@@ -628,7 +1035,7 @@ export class AiAssistantService {
|
||||
}
|
||||
}
|
||||
|
||||
if (nameField) {
|
||||
if (nameField && !extracted[nameField.apiName]) {
|
||||
const nameMatch = message.match(/add\s+([^\n]+?)(?:\s+with\s+|$)/i);
|
||||
if (nameMatch?.[1]) {
|
||||
extracted[nameField.apiName] = nameMatch[1].trim();
|
||||
@@ -646,6 +1053,8 @@ export class AiAssistantService {
|
||||
extracted[nameField.apiName] = message.replace(/^add\s+/i, '').trim();
|
||||
}
|
||||
|
||||
console.log('Heuristic extraction result:', extracted);
|
||||
|
||||
return extracted;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface AiAssistantReply {
|
||||
|
||||
export interface AiAssistantState {
|
||||
message: string;
|
||||
messages?: any[]; // BaseMessage[] from langchain - used when invoked by Deep Agent
|
||||
history?: AiChatMessage[];
|
||||
context: AiChatContext;
|
||||
objectDefinition?: any;
|
||||
|
||||
Reference in New Issue
Block a user