From fb23b64df26ae2ec449a983c8d81f8763472b39c Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Sun, 12 Apr 2026 16:15:45 +0800 Subject: [PATCH] feat: Redis tool cache, Service Bus alerts, doc-creator agent for edit/translate --- langgraph/package.json | 6 +- langgraph/pnpm-lock.yaml | 358 +++++++++++++++++- .../agent/enterprise/nodes/tool-executor.ts | 45 ++- langgraph/src/agent/utils/config.ts | 20 + .../src/agent/utils/doc-creator-client.ts | 42 ++ langgraph/src/agent/utils/redis-client.ts | 50 +++ langgraph/src/agent/utils/retry.ts | 14 + .../src/agent/utils/service-bus-client.ts | 38 ++ .../src/agent/writer/nodes/tool-executor.ts | 85 +++-- 9 files changed, 605 insertions(+), 53 deletions(-) create mode 100644 langgraph/src/agent/utils/doc-creator-client.ts create mode 100644 langgraph/src/agent/utils/redis-client.ts create mode 100644 langgraph/src/agent/utils/service-bus-client.ts diff --git a/langgraph/package.json b/langgraph/package.json index f8954c4..ac7d91b 100644 --- a/langgraph/package.json +++ b/langgraph/package.json @@ -21,8 +21,8 @@ "@assistant-ui/react": "^0.8.0", "@assistant-ui/react-markdown": "^0.8.0", "@assistant-ui/react-syntax-highlighter": "^0.7.2", + "@azure/service-bus": "^7.9.5", "@azure/storage-blob": "^12.31.0", - "@faker-js/faker": "^9.5.1", "@langchain/anthropic": "^0.3.18", "@langchain/core": "^0.3.45", "@langchain/google-genai": "^0.1.10", @@ -46,11 +46,11 @@ "esbuild": "^0.25.0", "esbuild-plugin-tailwindcss": "^2.0.1", "framer-motion": "^12.4.9", + "ioredis": "^5.10.1", "katex": "^0.16.21", "lucide-react": "^0.476.0", "next-themes": "^0.4.4", "pdf-parse": "^2.4.5", - "prettier": "^3.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.0.1", @@ -71,6 +71,7 @@ }, "devDependencies": { "@eslint/js": "^9.19.0", + "@faker-js/faker": "^9.5.1", "@types/node": "^22.13.5", "@types/pdf-parse": "^1.1.5", "@types/react": "^19.0.8", @@ -83,6 +84,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.18", "globals": "^15.14.0", + "prettier": "^3.5.2", "tailwind-scrollbar": "^4.0.1", "tailwindcss": "^4.0.6", "typescript": "~5.7.2", diff --git a/langgraph/pnpm-lock.yaml b/langgraph/pnpm-lock.yaml index 5bde838..f0a101d 100644 --- a/langgraph/pnpm-lock.yaml +++ b/langgraph/pnpm-lock.yaml @@ -17,12 +17,12 @@ importers: '@assistant-ui/react-syntax-highlighter': specifier: ^0.7.2 version: 0.7.10(@assistant-ui/react-markdown@0.8.0(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-syntax-highlighter@15.5.13)(@types/react@19.0.10)(react-syntax-highlighter@15.6.1(react@19.0.0))(react@19.0.0) + '@azure/service-bus': + specifier: ^7.9.5 + version: 7.9.5 '@azure/storage-blob': specifier: ^12.31.0 version: 12.31.0 - '@faker-js/faker': - specifier: ^9.5.1 - version: 9.5.1 '@langchain/anthropic': specifier: ^0.3.18 version: 0.3.18(@langchain/core@0.3.45(openai@4.95.1(zod@3.24.2))) @@ -92,6 +92,9 @@ importers: framer-motion: specifier: ^12.4.9 version: 12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + ioredis: + specifier: ^5.10.1 + version: 5.10.1 katex: specifier: ^0.16.21 version: 0.16.21 @@ -104,9 +107,6 @@ importers: pdf-parse: specifier: ^2.4.5 version: 2.4.5 - prettier: - specifier: ^3.5.2 - version: 3.5.2 react: specifier: ^19.0.0 version: 19.0.0 @@ -162,6 +162,9 @@ importers: '@eslint/js': specifier: ^9.19.0 version: 9.21.0 + '@faker-js/faker': + specifier: ^9.5.1 + version: 9.5.1 '@types/node': specifier: ^22.13.5 version: 22.13.5 @@ -198,6 +201,9 @@ importers: globals: specifier: ^15.14.0 version: 15.15.0 + prettier: + specifier: ^3.5.2 + version: 3.5.2 tailwind-scrollbar: specifier: ^4.0.1 version: 4.0.1(react@19.0.0)(tailwindcss@4.0.9) @@ -273,10 +279,18 @@ packages: '@types/react-dom': optional: true + '@azure/abort-controller@1.1.0': + resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==} + engines: {node: '>=12.0.0'} + '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} + '@azure/core-amqp@4.4.1': + resolution: {integrity: sha512-eiVwGOMpHWPS6YsX0kjW4rfH+f0Pb5L2EKNDbuXldVkuFKSEfROdl81xHLsMAl5PP5wiiTjErcMcKsJqwyaRqw==} + engines: {node: '>=20.0.0'} + '@azure/core-auth@1.10.1': resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} engines: {node: '>=20.0.0'} @@ -320,6 +334,10 @@ packages: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} + '@azure/service-bus@7.9.5': + resolution: {integrity: sha512-R5Af+4jtZZII2snLomaddMyElFtTCBRZp2qERPlP8PuISLU87eFYFM7xWzxjNd0yeiyQUBkamx/ZhOC8eWhCHA==} + engines: {node: '>=18.0.0'} + '@azure/storage-blob@12.31.0': resolution: {integrity: sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==} engines: {node: '>=20.0.0'} @@ -665,6 +683,9 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1374,6 +1395,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/is-buffer@2.0.2': + resolution: {integrity: sha512-G6OXy83Va+xEo8XgqAJYOuvOMxeey9xM5XKkvwJNmN8rVdcB+r15HvHsG86hl86JvU0y1aa7Z2ERkNFYWw9ySg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1600,6 +1624,10 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1627,6 +1655,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -1639,6 +1670,14 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1708,6 +1747,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + codepage@1.15.0: resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} engines: {node: '>=0.8'} @@ -1888,6 +1931,10 @@ packages: resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} engines: {node: '>=18'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -1896,6 +1943,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2165,6 +2216,10 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2213,6 +2268,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + generic-names@4.0.0: resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} @@ -2281,6 +2340,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2363,6 +2425,9 @@ packages: peerDependencies: postcss: ^8.1.0 + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2385,6 +2450,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -2397,9 +2466,21 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-decimal@1.0.4: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} @@ -2419,6 +2500,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2442,6 +2527,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2450,6 +2539,10 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} @@ -2507,6 +2600,9 @@ packages: engines: {node: '>=6'} hasBin: true + jssha@3.3.1: + resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==} + katex@0.16.21: resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==} hasBin: true @@ -2612,6 +2708,12 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2622,6 +2724,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3036,6 +3141,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-modules-extract-imports@3.1.0: resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} engines: {node: ^10 || ^12 || >= 14} @@ -3118,6 +3227,10 @@ packages: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -3252,6 +3365,14 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} @@ -3291,6 +3412,12 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rhea-promise@3.0.3: + resolution: {integrity: sha512-a875P5YcMkePSTEWMsnmCQS7Y4v/XvIw7ZoMtJxqtQRZsqSA6PsZxuz4vktyRykPuUgdNsA6F84dS3iEXZoYnQ==} + + rhea@3.0.4: + resolution: {integrity: sha512-n3kw8syCdrsfJ72w3rohpoHHlmv/RZZEP9VY5BVjjo0sEGIt4YSKypBgaiA+OUSgJAzLjOECYecsclG5xbYtZw==} + rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true @@ -3310,6 +3437,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -3332,6 +3463,10 @@ packages: serialize-query-params@2.0.2: resolution: {integrity: sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3387,6 +3522,9 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -3658,6 +3796,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -3768,6 +3909,10 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3952,10 +4097,30 @@ snapshots: - immer - use-sync-external-store + '@azure/abort-controller@1.1.0': + dependencies: + tslib: 2.8.1 + '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 + '@azure/core-amqp@4.4.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + rhea: 3.0.4 + rhea-promise: 3.0.3 + tslib: 2.8.1 + util: 0.12.5 + transitivePeerDependencies: + - supports-color + '@azure/core-auth@1.10.1': dependencies: '@azure/abort-controller': 2.1.2 @@ -4031,6 +4196,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@azure/service-bus@7.9.5': + dependencies: + '@azure/abort-controller': 1.1.0 + '@azure/core-amqp': 4.4.1 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/core-xml': 1.5.1 + '@azure/logger': 1.3.0 + '@types/is-buffer': 2.0.2 + buffer: 6.0.3 + is-buffer: 2.0.5 + jssha: 3.3.1 + long: 5.3.2 + process: 0.11.10 + rhea-promise: 3.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@azure/storage-blob@12.31.0': dependencies: '@azure/abort-controller': 2.1.2 @@ -4353,6 +4541,8 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} + '@ioredis/commands@1.5.1': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5049,6 +5239,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/is-buffer@2.0.2': + dependencies: + '@types/node': 22.13.5 + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -5316,6 +5510,10 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -5344,6 +5542,11 @@ snapshots: buffer-crc32@0.2.13: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -5355,6 +5558,18 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camelcase@6.3.0: {} @@ -5411,6 +5626,8 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + codepage@1.15.0: {} color-convert@1.9.3: @@ -5549,10 +5766,18 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.0 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@3.0.0: {} delayed-stream@1.0.0: {} + denque@2.1.0: {} + dequal@2.0.3: {} detect-libc@1.0.3: {} @@ -5860,6 +6085,10 @@ snapshots: fn.name@1.1.0: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -5899,6 +6128,8 @@ snapshots: function-bind@1.1.2: {} + generator-function@2.0.1: {} + generic-names@4.0.0: dependencies: loader-utils: 3.3.1 @@ -5969,6 +6200,10 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -6101,6 +6336,8 @@ snapshots: dependencies: postcss: 8.5.3 + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.1: @@ -6116,6 +6353,20 @@ snapshots: internmap@2.0.3: {} + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -6130,8 +6381,17 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-arrayish@0.3.2: {} + is-buffer@2.0.5: {} + + is-callable@1.2.7: {} + is-decimal@1.0.4: {} is-decimal@2.0.1: {} @@ -6142,6 +6402,14 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -6158,10 +6426,21 @@ snapshots: is-plain-obj@4.1.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-stream@2.0.1: {} is-stream@4.0.1: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + is-unicode-supported@2.1.0: {} is-what@4.1.16: {} @@ -6204,6 +6483,8 @@ snapshots: json5@2.2.3: {} + jssha@3.3.1: {} + katex@0.16.21: dependencies: commander: 8.3.0 @@ -6295,6 +6576,10 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -6308,6 +6593,8 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -6933,6 +7220,8 @@ snapshots: picomatch@4.0.4: {} + possible-typed-array-names@1.1.0: {} + postcss-modules-extract-imports@3.1.0(postcss@8.5.3): dependencies: postcss: 8.5.3 @@ -7007,6 +7296,8 @@ snapshots: prismjs@1.29.0: {} + process@0.11.10: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -7177,6 +7468,12 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + refractor@3.6.0: dependencies: hastscript: 6.0.0 @@ -7246,6 +7543,20 @@ snapshots: reusify@1.1.0: {} + rhea-promise@3.0.3: + dependencies: + debug: 4.4.3 + rhea: 3.0.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + rhea@3.0.4: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + rimraf@5.0.10: dependencies: glob: 10.4.5 @@ -7283,6 +7594,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} scheduler@0.25.0: {} @@ -7295,6 +7612,15 @@ snapshots: serialize-query-params@2.0.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7336,6 +7662,8 @@ snapshots: dependencies: type-fest: 0.7.1 + standard-as-callback@2.1.0: {} + std-env@3.10.0: {} string-hash@1.1.3: {} @@ -7588,6 +7916,14 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.20 + uuid@10.0.0: {} uuid@11.1.0: {} @@ -7713,6 +8049,16 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/langgraph/src/agent/enterprise/nodes/tool-executor.ts b/langgraph/src/agent/enterprise/nodes/tool-executor.ts index 33ccab6..4d615ad 100644 --- a/langgraph/src/agent/enterprise/nodes/tool-executor.ts +++ b/langgraph/src/agent/enterprise/nodes/tool-executor.ts @@ -32,6 +32,8 @@ import { } from "./tool-defs.js"; import { executeWithRetry, formatToolError } from "@/agent/utils/retry"; import { getToolConfig } from "@/agent/utils/toolConfig"; +import { redisGet, redisSet } from "@/agent/utils/redis-client"; +import crypto from "crypto"; /** Semantic sort order: lower = shown first. * error(100) → chart(200) → knowledge(300) → ticket(400) → search(500) → code(600) → doc(700) → actions(900) @@ -427,18 +429,34 @@ export async function toolExecutorNode( const kbCfg = getToolConfig("kb_search"); let kbRetryCount = 0; - try { - kbData = await withTimeout( - () => - executeWithRetry( - () => kbSearch(parsed.query), - kbCfg.maxRetries, - { backoffMs: kbCfg.backoffMs, exponential: kbCfg.exponential, onAttempt: () => { kbRetryCount++; } }, - ), - kbCfg.timeoutMs, - runSignal, - ); - } catch (kbError) { + // Redis cache: check for cached result before calling API + const cacheKey = `tool:kb_search:${crypto.createHash("md5").update(parsed.query).digest("hex")}`; + const cached = await redisGet(cacheKey); + if (cached) { + try { + kbData = JSON.parse(cached); + } catch { + kbData = null; + } + } + + if (!kbData) { + try { + kbData = await withTimeout( + () => + executeWithRetry( + () => kbSearch(parsed.query), + kbCfg.maxRetries, + { backoffMs: kbCfg.backoffMs, exponential: kbCfg.exponential, onAttempt: () => { kbRetryCount++; } }, + ), + kbCfg.timeoutMs, + runSignal, + ); + // Write successful result to cache (30min TTL) + if (kbData) { + await redisSet(cacheKey, JSON.stringify(kbData), 1800); + } + } catch (kbError) { // Push friendly error card, then try fallback to google_search ui.push( { @@ -547,8 +565,9 @@ export async function toolExecutorNode( }; } } + } // close if (!kbData) - const results = (kbData.results ?? []).slice(0, 5).map((r) => ({ + const results = (kbData!.results ?? []).slice(0, 5).map((r) => ({ title: r.title, category: r.category, snippet: r.content?.slice(0, 200) ?? "", diff --git a/langgraph/src/agent/utils/config.ts b/langgraph/src/agent/utils/config.ts index 1681d5a..c65299a 100644 --- a/langgraph/src/agent/utils/config.ts +++ b/langgraph/src/agent/utils/config.ts @@ -68,6 +68,12 @@ function validate() { ); } } + + // Optional infrastructure services — tool-level warn, not fatal + if (!process.env.REDIS_CONNECTION_STRING) console.warn("[config] REDIS_CONNECTION_STRING not set — tool result cache disabled"); + if (!process.env.SERVICEBUS_CONNECTION_STRING) console.warn("[config] SERVICEBUS_CONNECTION_STRING not set — alert notifications disabled"); + if (!process.env.AZURE_STORAGE_CONNECTION_STRING) console.warn("[config] AZURE_STORAGE_CONNECTION_STRING not set — file upload disabled"); + if (!process.env.DOC_CREATOR_API_KEY) console.warn("[config] DOC_CREATOR_API_KEY not set — doc-creator disabled, will fallback to LLM"); } // Run at import time @@ -106,4 +112,18 @@ export const config = { apiKey: process.env.DAYTONA_API_KEY ?? "", apiUrl: process.env.DAYTONA_API_URL ?? "https://app.daytona.io/api", }, + redis: { + connectionString: process.env.REDIS_CONNECTION_STRING ?? "", + }, + serviceBus: { + connectionString: process.env.SERVICEBUS_CONNECTION_STRING ?? "", + }, + storage: { + connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING ?? "", + }, + docCreator: { + endpoint: process.env.DOC_CREATOR_ENDPOINT ?? "", + apiKey: process.env.DOC_CREATOR_API_KEY ?? "", + fallbackEndpoint: process.env.DOC_CREATOR_FALLBACK_ENDPOINT ?? "", + }, } as const; diff --git a/langgraph/src/agent/utils/doc-creator-client.ts b/langgraph/src/agent/utils/doc-creator-client.ts new file mode 100644 index 0000000..333dc3c --- /dev/null +++ b/langgraph/src/agent/utils/doc-creator-client.ts @@ -0,0 +1,42 @@ +import { config } from "@/agent/utils/config"; + +interface DocCreatorParams { + type: "edit" | "translate"; + content: string; + instructions?: string; + targetLanguage?: string; + title?: string; +} + +export async function callDocCreator(params: DocCreatorParams): Promise { + const { endpoint, apiKey } = config.docCreator; + if (!endpoint || !apiKey) throw new Error("doc-creator not configured"); + + const body = { + inputs: { + title: params.title ?? "文档", + content: params.content, + type: params.type, + ...(params.instructions ? { instructions: params.instructions } : {}), + ...(params.targetLanguage ? { target_language: params.targetLanguage } : {}), + }, + query: params.instructions ?? `将文档翻译为${params.targetLanguage}`, + response_mode: "blocking", + conversation_id: "", + }; + + const resp = await fetch(`${endpoint}/api/chat-messages`, { + method: "POST", + headers: { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(30000), + }); + + if (!resp.ok) throw new Error(`doc-creator API failed: ${resp.status}`); + const data = await resp.json() as { answer?: string }; + if (!data.answer) throw new Error("doc-creator returned empty answer"); + return data.answer; +} diff --git a/langgraph/src/agent/utils/redis-client.ts b/langgraph/src/agent/utils/redis-client.ts new file mode 100644 index 0000000..40bf50a --- /dev/null +++ b/langgraph/src/agent/utils/redis-client.ts @@ -0,0 +1,50 @@ +import Redis from "ioredis"; +import { config } from "@/agent/utils/config"; + +let _redis: Redis | null = null; + +function parseAzureRedisConnectionString(connStr: string): { host: string; port: number; password: string } | null { + if (!connStr) return null; + try { + // Format: "host:port,password=xxx,ssl=True,abortConnect=False" + const parts = connStr.split(","); + const [host, portStr] = parts[0].split(":"); + const password = parts.find(p => p.startsWith("password="))?.split("=").slice(1).join("=") ?? ""; + return { host, port: parseInt(portStr ?? "6380"), password }; + } catch { + return null; + } +} + +export function getRedisClient(): Redis | null { + if (_redis) return _redis; + const parsed = parseAzureRedisConnectionString(config.redis.connectionString); + if (!parsed) return null; + _redis = new Redis({ + host: parsed.host, + port: parsed.port, + password: parsed.password, + tls: {}, + lazyConnect: true, + enableOfflineQueue: false, + connectTimeout: 3000, + }); + _redis.on("error", (e) => console.error("[redis] connection error:", e.message)); + return _redis; +} + +/** Get cached value, returns null on miss or error */ +export async function redisGet(key: string): Promise { + try { + return await getRedisClient()?.get(key) ?? null; + } catch { + return null; + } +} + +/** Set value with TTL in seconds, silently fails on error */ +export async function redisSet(key: string, value: string, ttlSeconds: number): Promise { + try { + await getRedisClient()?.set(key, value, "EX", ttlSeconds); + } catch { /* ignore */ } +} diff --git a/langgraph/src/agent/utils/retry.ts b/langgraph/src/agent/utils/retry.ts index 0191b70..9f2733c 100644 --- a/langgraph/src/agent/utils/retry.ts +++ b/langgraph/src/agent/utils/retry.ts @@ -137,5 +137,19 @@ export function formatToolError(toolName: string, error: unknown): string { } // Log raw error for debugging but return friendly message to LLM console.error(`[tool-error] ${toolName} (${kind}):`, error); + + // Fire-and-forget Service Bus alert for auth/server errors + if (kind === "auth" || kind === "server") { + import("@/agent/utils/service-bus-client").then(({ sendAlertAsync }) => { + sendAlertAsync({ + level: "error", + tool: toolName, + errorCode: kind.toUpperCase(), + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }); + }).catch(() => { /* ignore import failure */ }); + } + return friendly; } diff --git a/langgraph/src/agent/utils/service-bus-client.ts b/langgraph/src/agent/utils/service-bus-client.ts new file mode 100644 index 0000000..a44fbdb --- /dev/null +++ b/langgraph/src/agent/utils/service-bus-client.ts @@ -0,0 +1,38 @@ +import { ServiceBusClient, ServiceBusSender } from "@azure/service-bus"; +import { config } from "@/agent/utils/config"; + +let _client: ServiceBusClient | null = null; +let _sender: ServiceBusSender | null = null; +const QUEUE_NAME = "soc-alerts"; + +function getSender(): ServiceBusSender | null { + if (_sender) return _sender; + if (!config.serviceBus.connectionString) return null; + try { + _client = new ServiceBusClient(config.serviceBus.connectionString); + _sender = _client.createSender(QUEUE_NAME); + return _sender; + } catch { + return null; + } +} + +export interface AlertPayload { + level: "error" | "warn"; + tool: string; + errorCode?: string; + errorMessage: string; + traceId?: string; + timestamp: number; +} + +/** Fire-and-forget alert, never throws */ +export function sendAlertAsync(payload: AlertPayload): void { + const sender = getSender(); + if (!sender) return; + sender.sendMessages({ + body: payload, + contentType: "application/json", + subject: `soc-alert:${payload.level}:${payload.tool}`, + }).catch((e: Error) => console.error("[service-bus] send failed:", e.message)); +} diff --git a/langgraph/src/agent/writer/nodes/tool-executor.ts b/langgraph/src/agent/writer/nodes/tool-executor.ts index da13c8f..994ab4b 100644 --- a/langgraph/src/agent/writer/nodes/tool-executor.ts +++ b/langgraph/src/agent/writer/nodes/tool-executor.ts @@ -16,6 +16,7 @@ import { replyDraftSchema, } from "./tool-defs.js"; import { formatToolError } from "@/agent/utils/retry"; +import { callDocCreator } from "@/agent/utils/doc-creator-client"; /** * Search backwards through messages to find the most recent canvas-doc content @@ -131,27 +132,37 @@ export async function writerToolExecutorNode( const parsed = docEditSchema.parse(args); const existing = findDocContent(state, parsed.doc_id); - const editLlm = createLlm({ modelMode, maxTokens: 8192 }); const originalContent = existing?.content ?? "(原文档内容不可用)"; const originalTitle = existing?.title ?? "文档"; const docType = existing?.type ?? "markdown"; - const editResult = await editLlm.invoke([ - { - role: "system", - content: - "你是文档编辑助手。根据用户指令修改以下文档,直接输出修改后的完整文档内容,不要添加任何解释或说明。", - }, - { - role: "user", - content: `原文档标题: ${originalTitle}\n原文档内容:\n${originalContent}\n\n编辑指令: ${parsed.instructions}\n\n请直接输出修改后的完整文档内容:`, - }, - ]); - - const editedContent = - typeof editResult.content === "string" - ? editResult.content - : ""; + let editedContent: string; + try { + editedContent = await callDocCreator({ + type: "edit", + content: originalContent, + instructions: parsed.instructions, + title: originalTitle, + }); + } catch (docCreatorErr) { + console.warn("[writer] doc-creator failed, falling back to LLM:", docCreatorErr); + const editLlm = createLlm({ modelMode, maxTokens: 8192 }); + const editResult = await editLlm.invoke([ + { + role: "system", + content: + "你是文档编辑助手。根据用户指令修改以下文档,直接输出修改后的完整文档内容,不要添加任何解释或说明。", + }, + { + role: "user", + content: `原文档标题: ${originalTitle}\n原文档内容:\n${originalContent}\n\n编辑指令: ${parsed.instructions}\n\n请直接输出修改后的完整文档内容:`, + }, + ]); + editedContent = + typeof editResult.content === "string" + ? editResult.content + : ""; + } ui.push( { @@ -193,26 +204,36 @@ export async function writerToolExecutorNode( const parsed = docTranslateSchema.parse(args); const existing = findDocContent(state, parsed.doc_id); - const translateLlm = createLlm({ modelMode, maxTokens: 8192 }); const originalContent = existing?.content ?? "(原文档内容不可用)"; const originalTitle = existing?.title ?? "文档"; const docType = existing?.type ?? "markdown"; - const translateResult = await translateLlm.invoke([ - { - role: "system", - content: `你是专业翻译。将以下文档翻译为${parsed.target_language},保持原始格式和结构,直接输出翻译后的完整文档内容。`, - }, - { - role: "user", + let translatedContent: string; + try { + translatedContent = await callDocCreator({ + type: "translate", content: originalContent, - }, - ]); - - const translatedContent = - typeof translateResult.content === "string" - ? translateResult.content - : ""; + targetLanguage: parsed.target_language, + title: originalTitle, + }); + } catch (docCreatorErr) { + console.warn("[writer] doc-creator failed for translate, falling back to LLM:", docCreatorErr); + const translateLlm = createLlm({ modelMode, maxTokens: 8192 }); + const translateResult = await translateLlm.invoke([ + { + role: "system", + content: `你是专业翻译。将以下文档翻译为${parsed.target_language},保持原始格式和结构,直接输出翻译后的完整文档内容。`, + }, + { + role: "user", + content: originalContent, + }, + ]); + translatedContent = + typeof translateResult.content === "string" + ? translateResult.content + : ""; + } const translatedTitle = `${originalTitle} (${parsed.target_language})`;