From 2e925bc432cbe00538813d89e50609b3de4e858e Mon Sep 17 00:00:00 2001 From: nomeforme <143119811+nomeforme@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:45:00 +0100 Subject: [PATCH 1/6] commit --- financial-agent/.env.example | 4 +- financial-agent/.gitignore | 6 +- financial-agent/package-lock.json | 328 ++++++++++++++++++++++++------ 3 files changed, 279 insertions(+), 59 deletions(-) diff --git a/financial-agent/.env.example b/financial-agent/.env.example index 6213bba..c488001 100644 --- a/financial-agent/.env.example +++ b/financial-agent/.env.example @@ -15,4 +15,6 @@ AGENT_URL=http://localhost:3000 NVM_ENV=sandbox # or live SUBSCRIBER_NVM_API_KEY=your-subscriber-api-key NVM_PLAN_ID=your-plan-id -NVM_AGENT_ID=your-agent-id \ No newline at end of file +NVM_AGENT_ID=your-agent-id + +HELICONE_URL=https://helicone.nevermined.dev \ No newline at end of file diff --git a/financial-agent/.gitignore b/financial-agent/.gitignore index 319e5c4..7b65050 100644 --- a/financial-agent/.gitignore +++ b/financial-agent/.gitignore @@ -3,4 +3,8 @@ dist/ .env .DS_Store *.log -.vscode \ No newline at end of file +.vscode + +.env.local +.env.staging +.env.production \ No newline at end of file diff --git a/financial-agent/package-lock.json b/financial-agent/package-lock.json index 86c0cdb..cd47853 100644 --- a/financial-agent/package-lock.json +++ b/financial-agent/package-lock.json @@ -22,46 +22,178 @@ "typescript": "^5.6.2" } }, - "../../payments": { - "name": "@nevermined-io/payments", - "version": "1.0.0-rc14", - "license": "Apache-2.0", + "node_modules/@a2a-js/sdk": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.2.5.tgz", + "integrity": "sha512-VTDuRS5V0ATbJ/LkaQlisMnTAeYKXAK6scMguVBstf+KIBQ7HIuKhiXLv+G/hvejkV+THoXzoNifInAkU81P1g==", "dependencies": { - "@a2a-js/sdk": "^0.2.5", - "@helicone/helpers": "^1.6.0", - "axios": "^1.7.7", - "express": "4.21.2", - "jose": "^5.2.4", - "js-file-download": "^0.4.12", - "uuid": "^10.0.0", - "zod": "^4.0.17" + "@types/cors": "^2.8.17", + "@types/express": "^4.17.23", + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "express": "^4.21.2", + "uuid": "^11.1.0" }, - "devDependencies": { - "@babel/core": "^7.27.4", - "@babel/preset-env": "^7.27.2", - "@modelcontextprotocol/sdk": "^1.17.2", - "@types/express": "4.17.23", - "@types/jest": "^29.5.13", - "@types/node": "^20.11.19", - "@types/uuid": "^10.0.0", - "@types/ws": "^8.0.3", - "@typescript-eslint/eslint-plugin": "^5.62.0", - "@typescript-eslint/parser": "^5.62.0", - "babel-jest": "^30.0.2", - "eslint": "^8.56.0", - "eslint-config-nevermined": "^0.3.0", - "eslint-config-next": "^15.1.5", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-tsdoc": "^0.2.17", - "jest": "^29.7.0", - "prettier": "^3.2.5", - "source-map-support": "^0.5.21", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "tslib": "^2.6.2", - "typedoc": "0.25.13", - "typescript": "^5.3.3" + "engines": { + "node": ">=18" + } + }, + "node_modules/@a2a-js/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@a2a-js/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@a2a-js/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@a2a-js/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@a2a-js/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@a2a-js/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@a2a-js/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@a2a-js/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@a2a-js/sdk/node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@a2a-js/sdk/node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@a2a-js/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@a2a-js/sdk/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" } }, "node_modules/@cfworker/json-schema": { @@ -613,14 +745,65 @@ } }, "node_modules/@nevermined-io/payments": { - "resolved": "../../payments", - "link": true + "version": "1.0.0-rc14", + "resolved": "https://registry.npmjs.org/@nevermined-io/payments/-/payments-1.0.0-rc14.tgz", + "integrity": "sha512-BB7zes4tc6RpHJ4R205/1fztMDuSUAeBmACQrj+U+qFFlnXo/LFCKZe1hygWLYp0itK630o67u6ptaYH9G1IzQ==", + "license": "Apache-2.0", + "dependencies": { + "@a2a-js/sdk": "^0.2.5", + "@helicone/helpers": "^1.6.0", + "axios": "^1.7.7", + "express": "4.21.2", + "jose": "^5.2.4", + "js-file-download": "^0.4.12", + "uuid": "^10.0.0", + "zod": "^4.0.17" + } + }, + "node_modules/@nevermined-io/payments/node_modules/@helicone/helpers": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@helicone/helpers/-/helpers-1.7.1.tgz", + "integrity": "sha512-vXZ7vGs9WLl/PUYLcDFOv09jZjClsPbhnCqjqLX120GseOXnf1h5fo1BvCwGg/2A5Ty3XiwO9iFwtXeZfbUbzw==", + "license": "Apache-2.0", + "peerDependencies": { + "openai": "^5.10.2" + } + }, + "node_modules/@nevermined-io/payments/node_modules/openai": { + "version": "5.20.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.20.3.tgz", + "integrity": "sha512-8V0KgAcPFppDIP8uMBOkhRrhDBuxNQYQxb9IovP4NN4VyaYGISAzYexyYYuAwVul2HB75Wpib0xDboYJqRMNow==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@nevermined-io/payments/node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -631,7 +814,15 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -641,7 +832,6 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -654,7 +844,6 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -667,14 +856,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -700,14 +887,12 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/retry": { @@ -720,7 +905,6 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -731,7 +915,6 @@ "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -817,8 +1000,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -1051,6 +1232,19 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1330,8 +1524,6 @@ } ], "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=4.0" }, @@ -1581,6 +1773,21 @@ "node": ">= 0.10" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-file-download": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", + "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==", + "license": "MIT" + }, "node_modules/js-tiktoken": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", @@ -2100,6 +2307,15 @@ } } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2257,9 +2473,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/qs": { "version": "6.13.0", From 106a9d5d443744e80f347e8827d28b55a88c5590 Mon Sep 17 00:00:00 2001 From: hello Date: Tue, 23 Sep 2025 15:36:38 +0200 Subject: [PATCH 2/6] financial agent tutorial wip refactor --- financial-agent/agent/index_nevermined.ts | 278 ++++++++------------ financial-agent/agent/index_unprotected.ts | 163 ++++++------ financial-agent/client/index_nevermined.ts | 221 +++++++++------- financial-agent/client/index_unprotected.ts | 108 ++++---- 4 files changed, 377 insertions(+), 393 deletions(-) diff --git a/financial-agent/agent/index_nevermined.ts b/financial-agent/agent/index_nevermined.ts index 5d261d3..5212bb1 100644 --- a/financial-agent/agent/index_nevermined.ts +++ b/financial-agent/agent/index_nevermined.ts @@ -1,50 +1,42 @@ /** - * @fileoverview HTTP server for a financial-advice agent using LangChain and OpenAI. - * Exposes a `/ask` endpoint with per-session conversational memory. + * @fileoverview HTTP server for a financial-advice agent using OpenAI. + * Exposes a `/ask` endpoint with per-session conversational memory and Nevermined protection. */ import "dotenv/config"; import express, { Request, Response } from "express"; -import { ChatOpenAI } from "@langchain/openai"; -import { - ChatPromptTemplate, - MessagesPlaceholder, -} from "@langchain/core/prompts"; -import { RunnableWithMessageHistory } from "@langchain/core/runnables"; -import { InMemoryChatMessageHistory } from "@langchain/core/chat_history"; +import OpenAI from "openai"; import crypto from "crypto"; -import { - Payments, - EnvironmentName, - StartAgentRequest, -} from "@nevermined-io/payments"; +import { Payments, EnvironmentName, StartAgentRequest } from "@nevermined-io/payments"; -/** - * In-memory session message store. - */ -class SessionStore { - private sessions: Map = new Map(); +const app = express(); +app.use(express.json()); - /** - * Get or create the message history for a session id. - * @param {string} sessionId - Session identifier - * @returns {InMemoryChatMessageHistory} The chat message history for the session - */ - getHistory(sessionId: string): InMemoryChatMessageHistory { - let history = this.sessions.get(sessionId); - if (!history) { - history = new InMemoryChatMessageHistory(); - this.sessions.set(sessionId, history); - } - return history; - } +const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; +const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; +const NVM_API_KEY = process.env.BUILDER_NVM_API_KEY ?? ""; +const NVM_ENV = (process.env.NVM_ENV || "staging_sandbox") as EnvironmentName; +const NVM_AGENT_ID = process.env.NVM_AGENT_ID ?? ""; +const NVM_AGENT_HOST = process.env.NVM_AGENT_HOST || `http://localhost:${PORT}`; + +if (!OPENAI_API_KEY) { + console.error("OPENAI_API_KEY is required to run the agent."); + process.exit(1); } -/** - * Build the financial advisor prompt template. - * @returns {ChatPromptTemplate} The composed chat prompt template - */ -function buildFinancialPrompt(): ChatPromptTemplate { - const systemText = `You are FinGuide, a professional financial advisor and market analyst specializing in cryptocurrency and traditional markets. +if (!NVM_API_KEY || !NVM_AGENT_ID) { + console.error("Nevermined environment is required: set NVM_API_KEY and NVM_AGENT_ID in .env"); + process.exit(1); +} + +// Initialize Nevermined Payments SDK for access control and observability +const payments = Payments.getInstance({ + nvmApiKey: NVM_API_KEY, + environment: NVM_ENV, +}); + +// Define the AI assistant's role and behavior +function getSystemPrompt(): string { + return `You are FinGuide, a professional financial advisor and market analyst specializing in cryptocurrency and traditional markets. Your role is to provide: 1. Real-time market data: current prices of cryptocurrencies, stock market performance, and key market indicators. @@ -77,151 +69,91 @@ Important constraints: - You provide financial information and general advice, not personalized financial planning. - Recommend consulting with a qualified financial advisor for personalized decisions. - Avoid collecting personally identifiable information. -- Ask clarifying questions when the user intent or constraints (budget, risk tolerance, time horizon) are unclear. -`; - return ChatPromptTemplate.fromMessages([ - ["system", systemText], - new MessagesPlaceholder("history"), - ["human", "{input}"], - ]); +- Ask clarifying questions when the user intent or constraints (budget, risk tolerance, time horizon) are unclear.`; } -/** - * Create LangChain pipeline with per-session memory. - * @param {ChatOpenAI} model - Chat model instance - * @returns {RunnableWithMessageHistory} Runnable with history - */ -function createRunnable(model: ChatOpenAI) { - const prompt = buildFinancialPrompt(); - const chain = prompt.pipe(model); - const runnable = new RunnableWithMessageHistory({ - runnable: chain, - getMessageHistory: async (sessionId: string) => - sessionStore.getHistory(sessionId), - inputMessagesKey: "input", - historyMessagesKey: "history", - }); - return runnable; -} +// Store conversation history for each session +const sessions = new Map(); -const app = express(); -app.use(express.json()); +// Handle financial advice requests with Nevermined payment protection and observability +app.post("/ask", async (req: Request, res: Response) => { + try { + // Extract authorization details from request headers + const authHeader = (req.headers["authorization"] || "") as string; + const requestedUrl = `${NVM_AGENT_HOST}${req.url}`; + const httpVerb = req.method; + + // Check if user is authorized and has sufficient balance + const agentRequest = await payments.requests.startProcessingRequest( + NVM_AGENT_ID, + authHeader, + requestedUrl, + httpVerb + ); -const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; -const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; -if (!OPENAI_API_KEY) { - // eslint-disable-next-line no-console - console.error("OPENAI_API_KEY is required to run the agent."); - process.exit(1); -} + // Reject request if user doesn't have credits or subscription + if (!agentRequest.balance.isSubscriber || agentRequest.balance.balance < 1n) { + return res.status(402).json({ error: "Payment Required" }); + } -// Nevermined required configuration -const NVM_API_KEY = process.env.BUILDER_NVM_API_KEY ?? ""; -const NVM_ENV = (process.env.NVM_ENV || "staging_sandbox") as EnvironmentName; -const NVM_AGENT_ID = process.env.NVM_AGENT_ID ?? ""; -const NVM_AGENT_HOST = process.env.NVM_AGENT_HOST || `http://localhost:${PORT}`; -const NVM_PLAN_ID = process.env.NVM_PLAN_ID ?? ""; + // Extract access token for credit redemption + const requestAccessToken = authHeader.replace(/^Bearer\s+/i, ""); -if (!NVM_API_KEY || !NVM_AGENT_ID) { - // eslint-disable-next-line no-console - console.error( - "Nevermined environment is required: set NVM_API_KEY and NVM_AGENT_ID in .env" - ); - process.exit(1); -} + // Extract and validate the user's input + const input = String(req.body?.input_query ?? "").trim(); + if (!input) return res.status(400).json({ error: "Missing input" }); -/** - * Build a singleton Payments client for Nevermined. - */ -const payments = Payments.getInstance({ - nvmApiKey: NVM_API_KEY, - environment: NVM_ENV, -}); + // Get or create a session ID for conversation continuity + let { sessionId } = req.body as { sessionId?: string }; + if (!sessionId) sessionId = crypto.randomUUID(); -const sessionStore = new SessionStore(); + // Retrieve existing conversation history or start fresh + let messages = sessions.get(sessionId) || []; -/** - * Create a model with dynamic sessionId and custom properties for each request - * @param {string} sessionId - The session ID for this request - * @param {Record} customProperties - Additional custom properties to include as headers - * @returns {ChatOpenAI} Configured ChatOpenAI model - */ -function createModelWithSessionId( - agentRequest: StartAgentRequest, - customProperties: Record = {} -): ChatOpenAI { - return new ChatOpenAI( - payments.observability.withHeliconeLangchain( - "gpt-4o-mini", + // Add system prompt if this is a new conversation + if (messages.length === 0) { + messages.push({ + role: "system", + content: getSystemPrompt() + }); + } + + // Add the user's question to the conversation + messages.push({ role: "user", content: input }); + + // Set up observability metadata for tracking this operation + const customProperties = { + agentid: NVM_AGENT_ID, + sessionid: sessionId, + credit_amount: "1", + credit_usd_rate: "0.001", + credit_price_usd: "0.001", + operation: "financial_advice", + }; + + // Create OpenAI client with Helicone observability integration + const openai = new OpenAI(payments.observability.withHeliconeOpenAI( OPENAI_API_KEY, agentRequest, customProperties - ) - ); -} - -/** - * Ensure the incoming request is authorized via Nevermined and return request data for redemption. - * @param {Request} req - Express request object - * @returns {{ agentRequestId: string, requestAccessToken: string }} identifiers to redeem credits later - * @throws Error with statusCode 402 when not authorized - */ -async function ensureAuthorized( - req: Request -): Promise<{ agentRequest: StartAgentRequest; requestAccessToken: string }> { - const authHeader = (req.headers["authorization"] || "") as string; - const requestedUrl = `${NVM_AGENT_HOST}${req.url}`; - const httpVerb = req.method; - const result = await payments.requests.startProcessingRequest( - NVM_AGENT_ID, - authHeader, - requestedUrl, - httpVerb - ); - if (!result.balance.isSubscriber || result.balance.balance < 1n) { - const error: any = new Error("Payment Required"); - error.statusCode = 402; - throw error; - } - const requestAccessToken = authHeader.replace(/^Bearer\s+/i, ""); - return { agentRequest: result, requestAccessToken }; -} - -/** - * POST /ask - * Body: { input: string, sessionId?: string } - * Returns: { output: string, sessionId: string } - */ -/** - * Handle medical question requests. - * Creates a session when one is not provided and reuses memory across calls. - */ -app.post("/ask", async (req: Request, res: Response) => { - try { - const { agentRequest, requestAccessToken } = await ensureAuthorized(req); - console.log("agentRequestId", agentRequest.agentRequestId); - console.log("requestAccessToken", requestAccessToken); - const input = String(req.body?.input_query ?? "").trim(); - if (!input) return res.status(400).json({ error: "Missing input" }); - - let { sessionId } = req.body as { sessionId?: string }; - if (!sessionId) sessionId = crypto.randomUUID(); + )); + + // Call OpenAI API to generate response + const completion = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: messages, + temperature: 0.3, + max_tokens: 1000, + }); - // Create model and runnable with the dynamic sessionId - const model = createModelWithSessionId(agentRequest); - const runnable = createRunnable(model); + // Extract the AI's response + const response = completion.choices[0]?.message?.content || "No response generated"; - const result = await runnable.invoke( - { input }, - { configurable: { sessionId } } - ); - const text = - result?.content ?? - (Array.isArray(result) - ? result.map((m: any) => m.content).join("\n") - : String(result)); + // Save the AI's response to conversation history + messages.push({ role: "assistant", content: response }); + sessions.set(sessionId, messages); - // After successful processing, redeem 1 credit for this request + // Redeem credits after successful API call let redemptionResult: any; try { redemptionResult = await payments.requests.redeemCreditsFromRequest( @@ -230,9 +162,7 @@ app.post("/ask", async (req: Request, res: Response) => { 1n ); redemptionResult.creditsRedeemed = 1; - console.log("redemptionResult", redemptionResult); } catch (redeemErr) { - // eslint-disable-next-line no-console console.error("Failed to redeem credits:", redeemErr); redemptionResult = { creditsRedeemed: 0, @@ -240,9 +170,9 @@ app.post("/ask", async (req: Request, res: Response) => { }; } - res.json({ output: text, sessionId, redemptionResult }); + // Return response with session info and payment details + res.json({ output: response, sessionId, redemptionResult }); } catch (error: any) { - // eslint-disable-next-line no-console console.error("Error handling /ask", error); const status = error?.statusCode === 402 ? 402 : 500; res.status(status).json({ @@ -251,11 +181,11 @@ app.post("/ask", async (req: Request, res: Response) => { } }); +// Health check endpoint app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok" }); }); app.listen(PORT, () => { - // eslint-disable-next-line no-console - console.log(`Agent listening on http://localhost:${PORT}`); + console.log(`Financial Agent listening on http://localhost:${PORT}`); }); diff --git a/financial-agent/agent/index_unprotected.ts b/financial-agent/agent/index_unprotected.ts index aa3c610..5de2551 100644 --- a/financial-agent/agent/index_unprotected.ts +++ b/financial-agent/agent/index_unprotected.ts @@ -1,111 +1,120 @@ /** - * @fileoverview Free-access HTTP server for the medical-advice agent (no Nevermined protection). + * @fileoverview Free-access HTTP server for the financial-advice agent (no Nevermined protection). * Provides a `/ask` endpoint with per-session conversational memory. */ import "dotenv/config"; import express, { Request, Response } from "express"; -import { ChatOpenAI } from "@langchain/openai"; -import { - ChatPromptTemplate, - MessagesPlaceholder, -} from "@langchain/core/prompts"; -import { RunnableWithMessageHistory } from "@langchain/core/runnables"; -import { InMemoryChatMessageHistory } from "@langchain/core/chat_history"; +import OpenAI from "openai"; import crypto from "crypto"; -class SessionStore { - private sessions: Map = new Map(); - getHistory(sessionId: string): InMemoryChatMessageHistory { - let history = this.sessions.get(sessionId); - if (!history) { - history = new InMemoryChatMessageHistory(); - this.sessions.set(sessionId, history); - } - return history; - } -} - -function buildMedicalPrompt(): ChatPromptTemplate { - const systemText = `You are MedGuide, a board-certified medical expert assistant. -Provide accurate, evidence-based, and empathetic medical guidance. -Constraints and behavior: -- You are not a substitute for a licensed physician or emergency services. -- If symptoms are severe, sudden, or life-threatening, advise calling emergency services immediately. -- Be concise but thorough. Use plain language, avoid jargon, and explain reasoning. -- Always ask clarifying questions when the information is insufficient. -- Provide differential considerations when appropriate and list red flags. -- Include lifestyle guidance and self-care measures when relevant. -- When medication is discussed, include typical adult dosage ranges where safe and general, and warn to consult a clinician for personalized dosing, interactions, or contraindications. -- Suggest when to seek in-person evaluation and what tests a clinician might order. -- Never provide definitive diagnoses. Use probabilistic language (e.g., likely, possible). -- Respect privacy and avoid collecting personally identifiable information. -- If the request is outside medical scope, politely decline or redirect.`; - return ChatPromptTemplate.fromMessages([ - ["system", systemText], - new MessagesPlaceholder("history"), - ["human", "{input}"], - ]); -} - -function createRunnable(model: ChatOpenAI) { - const prompt = buildMedicalPrompt(); - const chain = prompt.pipe(model); - return new RunnableWithMessageHistory({ - runnable: chain, - getMessageHistory: async (sessionId: string) => - sessionStore.getHistory(sessionId), - inputMessagesKey: "input", - historyMessagesKey: "history", - }); -} - const app = express(); app.use(express.json()); const PORT = process.env.PORT ? Number(process.env.PORT) : 3001; const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; + if (!OPENAI_API_KEY) { - // eslint-disable-next-line no-console - console.error("OPENAI_API_KEY is required to run the free agent."); + console.error("OPENAI_API_KEY is required to run the agent."); process.exit(1); } -const sessionStore = new SessionStore(); -const model = new ChatOpenAI({ - model: "gpt-4o-mini", - temperature: 0.3, - apiKey: OPENAI_API_KEY, -}); -const runnable = createRunnable(model); +// Initialize OpenAI client with API key +const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); + +// Define the AI assistant's role and behavior +function getSystemPrompt(): string { + return `You are FinGuide, a professional financial advisor and market analyst specializing in cryptocurrency and traditional markets. +Your role is to provide: + +1. Real-time market data: current prices of cryptocurrencies, stock market performance, and key market indicators. +2. Investment analysis: monthly returns of major companies, market trends, and investment opportunities. +3. Financial advice: recommendations on whether to invest in specific assets based on current market conditions in a generic way. +4. Educational content: explain financial concepts, market dynamics, and investment strategies in simple terms. + +Response requirements: +- Be accurate and rely on current market data where possible. +- Include specific numbers and percentages when relevant. +- Provide balanced advice considering both opportunities and risks. +- Be educational and explain the reasoning behind recommendations. +- Always include appropriate risk warnings. +- Maintain a professional but accessible tone for both beginners and experienced investors. + +When providing investment advice: +- Consider the user's risk tolerance (no need to ask if not specified). +- Mention both potential gains and potential losses. +- Include time horizon recommendations. +- Suggest diversification strategies. +- Always remind: past performance does not guarantee future results. +Formatting: +- Use clear headings and bullet points. +- Display current market data prominently. +- Highlight risk warnings in bold. +- Provide actionable recommendations when appropriate. + +Important constraints: +- You provide financial information and general advice, not personalized financial planning. +- Recommend consulting with a qualified financial advisor for personalized decisions. +- Avoid collecting personally identifiable information. +- Ask clarifying questions when the user intent or constraints (budget, risk tolerance, time horizon) are unclear.`; +} + +// Store conversation history for each session +const sessions = new Map(); + +// Handle financial advice requests with session-based conversation memory app.post("/ask", async (req: Request, res: Response) => { try { + // Extract and validate the user's input const input = String(req.body?.input_query ?? "").trim(); if (!input) return res.status(400).json({ error: "Missing input" }); + + // Get or create a session ID for conversation continuity let { sessionId } = req.body as { sessionId?: string }; if (!sessionId) sessionId = crypto.randomUUID(); - const result = await runnable.invoke( - { input }, - { configurable: { sessionId } } - ); - const text = - result?.content ?? - (Array.isArray(result) - ? result.map((m: any) => m.content).join("\n") - : String(result)); - res.json({ output: text, sessionId }); + + // Retrieve existing conversation history or start fresh + let messages = sessions.get(sessionId) || []; + + // Add system prompt if this is a new conversation + if (messages.length === 0) { + messages.push({ + role: "system", + content: getSystemPrompt() + }); + } + + // Add the user's question to the conversation + messages.push({ role: "user", content: input }); + + // Call OpenAI API to generate response + const completion = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: messages, + temperature: 0.3, + max_tokens: 1000, + }); + + // Extract the AI's response + const response = completion.choices[0]?.message?.content || "No response generated"; + + // Save the AI's response to conversation history + messages.push({ role: "assistant", content: response }); + sessions.set(sessionId, messages); + + // Return response to the client + res.json({ output: response, sessionId }); } catch (error: any) { - // eslint-disable-next-line no-console - console.error("Free agent /ask error:", error); + console.error("Agent /ask error:", error); res.status(500).json({ error: "Internal server error" }); } }); +// Health check endpoint app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok" }); }); app.listen(PORT, () => { - // eslint-disable-next-line no-console - console.log(`Free Agent listening on http://localhost:${PORT}`); + console.log(`Financial Agent listening on http://localhost:${PORT}`); }); diff --git a/financial-agent/client/index_nevermined.ts b/financial-agent/client/index_nevermined.ts index 06e5e78..b9aa1ef 100644 --- a/financial-agent/client/index_nevermined.ts +++ b/financial-agent/client/index_nevermined.ts @@ -5,122 +5,147 @@ import "dotenv/config"; import { Payments, EnvironmentName } from "@nevermined-io/payments"; -/** - * Run the protected demo client. - * Sends predefined financial questions to the agent with Authorization and reuses sessionId to preserve context. - * @returns {Promise} Resolves when the run finishes - */ -async function main(): Promise { - const baseUrl = process.env.AGENT_URL || "http://localhost:3000"; +// Configuration: Load environment variables with defaults +const AGENT_URL = process.env.AGENT_URL || "http://localhost:3000"; +const PLAN_ID = process.env.NVM_PLAN_ID as string; +const AGENT_ID = process.env.NVM_AGENT_ID as string; +const SUBSCRIBER_API_KEY = process.env.SUBSCRIBER_NVM_API_KEY as string; +const NVM_ENVIRONMENT = (process.env.NVM_ENV || "sandbox") as EnvironmentName; - // Predefined questions for the demo client. The client is intentionally dumb. - const questions: string[] = [ - "What is your market outlook for Bitcoin over the next month?", - "How are major stock indices performing today and what trends are notable?", - "What risks should I consider before increasing exposure to tech stocks?", - ]; +// Define test questions to demonstrate conversation continuity +const TEST_QUESTIONS = [ + "What is your market outlook for Bitcoin over the next month?", + "How are major stock indices performing today and what trends are notable?", + "What risks should I consider before increasing exposure to tech stocks?", +]; - let sessionId: string | undefined; - let bearer: string | undefined; - - const planId = process.env.NVM_PLAN_ID as string; - const agentId = process.env.NVM_AGENT_ID as string; - const nvmApiKey = process.env.SUBSCRIBER_NVM_API_KEY as string; - const nvmEnv = (process.env.NVM_ENV || "sandbox") as EnvironmentName; - if (!planId || !agentId) { - throw new Error("NVM_PLAN_ID and NVM_AGENT_ID are required in client env"); +// Validate required environment variables +function validateEnvironment(): void { + if (!PLAN_ID || !AGENT_ID) { + throw new Error("NVM_PLAN_ID and NVM_AGENT_ID are required in environment"); } - if (!nvmApiKey) { - throw new Error("SUBSCRIBER_NVM_API_KEY is required in client env"); + if (!SUBSCRIBER_API_KEY) { + throw new Error("SUBSCRIBER_NVM_API_KEY is required in environment"); } - bearer = await getOrBuyAccessToken({ - planId, - agentId, - nvmApiKey, - nvmEnv, +} + +// Get or purchase access token for protected agent +async function getAccessToken(): Promise { + console.log("🔐 Setting up Nevermined access..."); + + // Initialize Nevermined Payments SDK + const payments = Payments.getInstance({ + nvmApiKey: SUBSCRIBER_API_KEY, + environment: NVM_ENVIRONMENT, }); - for (let i = 0; i < questions.length; i += 1) { - const input = questions[i]; - // eslint-disable-next-line no-console - console.log(`\n[CLIENT] Sending question ${i + 1}: ${input}`); - const response = await askAgent(baseUrl, input, sessionId, bearer); - sessionId = response.sessionId; - // eslint-disable-next-line no-console - console.log(`[AGENT] (sessionId=${sessionId})\n${response.output}`); + // Check current plan balance and subscription status + const balanceInfo: any = await payments.plans.getPlanBalance(PLAN_ID); + const hasCredits = Number(balanceInfo?.balance ?? 0) > 0; + const isSubscriber = balanceInfo?.isSubscriber === true; + + // Purchase plan if not subscribed and no credits + if (!isSubscriber && !hasCredits) { + console.log("💳 No subscription or credits found. Purchasing plan..."); + await payments.plans.orderPlan(PLAN_ID); + } + + // Get access token for the agent + const credentials = await payments.agents.getAgentAccessToken(PLAN_ID, AGENT_ID); + + if (!credentials?.accessToken) { + throw new Error("Failed to obtain access token"); } + + console.log("✅ Access token obtained successfully"); + return credentials.accessToken; } -/** - * Perform a POST /ask to the protected agent. - * @param {string} baseUrl - Base URL of the agent service - * @param {string} input - User question text - * @param {string} [sessionId] - Optional existing session id - * @param {string} [bearer] - Authorization token to access protected endpoint - * @returns {Promise<{ output: string; sessionId: string }>} The JSON response containing output and sessionId - */ -async function askAgent( - baseUrl: string, - input: string, - sessionId?: string, - bearer?: string -): Promise<{ output: string; sessionId: string }> { - const res = await fetch(`${baseUrl}/ask`, { +// Send a question to the protected financial agent +async function askAgent(input: string, accessToken: string, sessionId?: string): Promise<{ output: string; sessionId: string; redemptionResult?: any }> { + // Prepare request payload + const requestBody = { + input_query: input, + sessionId: sessionId + }; + + // Prepare headers with authorization + const headers = { + "Content-Type": "application/json", + "Authorization": `Bearer ${accessToken}` + }; + + // Make HTTP request to protected agent + const response = await fetch(`${AGENT_URL}/ask`, { method: "POST", - headers: { - "Content-Type": "application/json", - ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), - }, - body: JSON.stringify({ input_query: input, sessionId }), + headers: headers, + body: JSON.stringify(requestBody), }); - if (!res.ok) { - const errorText = await res.text().catch(() => ""); - throw new Error( - `Agent request failed: ${res.status} ${res.statusText} ${errorText}` - ); + + // Handle HTTP errors (including payment required) + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + if (response.status === 402) { + throw new Error("Payment Required - insufficient credits or subscription"); + } + throw new Error(`Agent request failed: ${response.status} ${response.statusText} ${errorText}`); } - const data = (await res.json()) as { output: string; sessionId: string }; - return data; + + // Parse and return JSON response + return await response.json() as { output: string; sessionId: string; redemptionResult?: any }; } /** - * Get a valid access token by checking plan balance/subscription first. - * If not subscribed or no credits, purchase the plan and then fetch the token. - * @param {Object} opts - Options object - * @param {string} opts.planId - Plan identifier - * @param {string} opts.agentId - Agent identifier - * @param {string} opts.nvmApiKey - Nevermined API Key (subscriber) - * @param {EnvironmentName} opts.nvmEnv - Nevermined environment - * @returns {Promise} The access token string + * Run the protected demo client. + * Sends predefined financial questions to the agent with Authorization and reuses sessionId to preserve context. + * @returns {Promise} Resolves when the run finishes */ -async function getOrBuyAccessToken(opts: { - planId: string; - agentId: string; - nvmApiKey: string; - nvmEnv: EnvironmentName; -}): Promise { - const payments = Payments.getInstance({ - nvmApiKey: opts.nvmApiKey, - environment: opts.nvmEnv, - }); - const balanceInfo: any = await payments.plans.getPlanBalance(opts.planId); - const hasCredits = Number(balanceInfo?.balance ?? 0) > 0; - const isSubscriber = balanceInfo?.isSubscriber === true; - if (!isSubscriber && !hasCredits) { - console.log("Ordering plan with key: ", opts.nvmApiKey); - await payments.plans.orderPlan(opts.planId); +async function runDemo(): Promise { + console.log("🚀 Starting Financial Agent Demo (Protected with Nevermined)\n"); + + // Validate environment setup + validateEnvironment(); + + // Obtain access token for protected agent + const accessToken = await getAccessToken(); + + // Track session across multiple questions + let sessionId: string | undefined; + + // Send each test question and maintain conversation context + for (let i = 0; i < TEST_QUESTIONS.length; i++) { + const question = TEST_QUESTIONS[i]; + + console.log(`📝 Question ${i + 1}: ${question}`); + + try { + // Send question to protected agent (reusing sessionId for context) + const result = await askAgent(question, accessToken, sessionId); + + // Update sessionId for next question + sessionId = result.sessionId; + + // Display agent response and payment info + console.log(`🤖 FinGuide (Session: ${sessionId}):`); + console.log(result.output); + + if (result.redemptionResult) { + console.log(`💰 Credits redeemed: ${result.redemptionResult.creditsRedeemed || 0}`); + } + + console.log("\n" + "=".repeat(80) + "\n"); + + } catch (error) { + console.error(`❌ Error processing question ${i + 1}:`, error); + break; + } } - const creds = await payments.agents.getAgentAccessToken( - opts.planId, - opts.agentId - ); - if (!creds?.accessToken) throw new Error("Access token unavailable"); - return creds.accessToken; + + console.log("✅ Demo completed!"); } -// Run the client -main().catch((err) => { - // eslint-disable-next-line no-console - console.error("[CLIENT] Error:", err); +// Run the demo and handle any errors +runDemo().catch((error) => { + console.error("💥 Demo failed:", error); process.exit(1); }); diff --git a/financial-agent/client/index_unprotected.ts b/financial-agent/client/index_unprotected.ts index 4db566f..6a6faea 100644 --- a/financial-agent/client/index_unprotected.ts +++ b/financial-agent/client/index_unprotected.ts @@ -4,61 +4,81 @@ */ import "dotenv/config"; +// Configuration: Agent URL from environment or default +const AGENT_URL = process.env.AGENT_URL || "http://localhost:3001"; + +// Define test questions to demonstrate conversation continuity +const TEST_QUESTIONS = [ + "What is your market outlook for Bitcoin over the next month?", + "How are major stock indices performing today and what trends are notable?", + "What risks should I consider before increasing exposure to tech stocks?", +]; + +// Send a question to the financial agent +async function askAgent(input: string, sessionId?: string): Promise<{ output: string; sessionId: string }> { + // Prepare request payload + const requestBody = { + input_query: input, + sessionId: sessionId + }; + + // Make HTTP request to agent + const response = await fetch(`${AGENT_URL}/ask`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + + // Handle HTTP errors + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + throw new Error(`Agent request failed: ${response.status} ${response.statusText} ${errorText}`); + } + + // Parse and return JSON response + return await response.json() as { output: string; sessionId: string }; +} + /** * Run the unprotected demo client. * Sends predefined financial questions to the agent and reuses sessionId to preserve context. * @returns {Promise} Resolves when the run finishes */ -async function main(): Promise { - const baseUrl = process.env.AGENT_URL || "http://localhost:3001"; - - const questions: string[] = [ - "What is your market outlook for Bitcoin over the next month?", - "How are major stock indices performing today and what trends are notable?", - "What risks should I consider before increasing exposure to tech stocks?", - ]; +async function runDemo(): Promise { + console.log("🚀 Starting Financial Agent Demo (Unprotected)\n"); + // Track session across multiple questions let sessionId: string | undefined; - for (let i = 0; i < questions.length; i += 1) { - const input = questions[i]; - // eslint-disable-next-line no-console - console.log(`\n[FREE CLIENT] Sending question ${i + 1}: ${input}`); - const response = await askAgent(baseUrl, input, sessionId); - sessionId = response.sessionId; - // eslint-disable-next-line no-console - console.log(`[FREE AGENT] (sessionId=${sessionId})\n${response.output}`); - } -} + // Send each test question and maintain conversation context + for (let i = 0; i < TEST_QUESTIONS.length; i++) { + const question = TEST_QUESTIONS[i]; -/** - * Perform a POST /ask to the free agent. - * @param {string} baseUrl - Base URL of the agent service - * @param {string} input - User question text - * @param {string} [sessionId] - Optional existing session id to keep context - * @returns {Promise<{ output: string; sessionId: string }>} Response with model output and session id - */ -async function askAgent( - baseUrl: string, - input: string, - sessionId?: string -): Promise<{ output: string; sessionId: string }> { - const res = await fetch(`${baseUrl}/ask`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ input_query: input, sessionId }), - }); - if (!res.ok) { - const errorText = await res.text().catch(() => ""); - throw new Error( - `Free agent request failed: ${res.status} ${res.statusText} ${errorText}` - ); + console.log(`📝 Question ${i + 1}: ${question}`); + + try { + // Send question to agent (reusing sessionId for context) + const result = await askAgent(question, sessionId); + + // Update sessionId for next question + sessionId = result.sessionId; + + // Display agent response + console.log(`🤖 FinGuide (Session: ${sessionId}):`); + console.log(result.output); + console.log("\n" + "=".repeat(80) + "\n"); + + } catch (error) { + console.error(`❌ Error processing question ${i + 1}:`, error); + break; + } } - return (await res.json()) as { output: string; sessionId: string }; + + console.log("✅ Demo completed!"); } -main().catch((err) => { - // eslint-disable-next-line no-console - console.error("[FREE CLIENT] Error:", err); +// Run the demo and handle any errors +runDemo().catch((error) => { + console.error("💥 Demo failed:", error); process.exit(1); }); From b53e50d478a3dc1c4be09db67655d6bec7c1f626 Mon Sep 17 00:00:00 2001 From: nomeforme <143119811+nomeforme@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:10:06 +0200 Subject: [PATCH 3/6] change system prompt to financial education agent --- financial-agent/agent/index_nevermined.ts | 62 +++++-------- financial-agent/agent/index_unprotected.ts | 50 ++++------- financial-agent/client/index_nevermined.ts | 99 +++++++++++++-------- financial-agent/client/index_unprotected.ts | 77 ++++++++++------ 4 files changed, 152 insertions(+), 136 deletions(-) diff --git a/financial-agent/agent/index_nevermined.ts b/financial-agent/agent/index_nevermined.ts index 5212bb1..31f503c 100644 --- a/financial-agent/agent/index_nevermined.ts +++ b/financial-agent/agent/index_nevermined.ts @@ -36,40 +36,23 @@ const payments = Payments.getInstance({ // Define the AI assistant's role and behavior function getSystemPrompt(): string { - return `You are FinGuide, a professional financial advisor and market analyst specializing in cryptocurrency and traditional markets. + return `You are FinGuide, a friendly financial education AI designed to help people learn about investing, personal finance, and market concepts. + Your role is to provide: -1. Real-time market data: current prices of cryptocurrencies, stock market performance, and key market indicators. -2. Investment analysis: monthly returns of major companies, market trends, and investment opportunities. -3. Financial advice: recommendations on whether to invest in specific assets based on current market conditions in a generic way. -4. Educational content: explain financial concepts, market dynamics, and investment strategies in simple terms. - -Response requirements: -- Be accurate and rely on current market data where possible. -- Include specific numbers and percentages when relevant. -- Provide balanced advice considering both opportunities and risks. -- Be educational and explain the reasoning behind recommendations. -- Always include appropriate risk warnings. -- Maintain a professional but accessible tone for both beginners and experienced investors. - -When providing investment advice: -- Consider the user's risk tolerance (no need to ask if not specified). -- Mention both potential gains and potential losses. -- Include time horizon recommendations. -- Suggest diversification strategies. -- Always remind: past performance does not guarantee future results. - -Formatting: -- Use clear headings and bullet points. -- Display current market data prominently. -- Highlight risk warnings in bold. -- Provide actionable recommendations when appropriate. - -Important constraints: -- You provide financial information and general advice, not personalized financial planning. -- Recommend consulting with a qualified financial advisor for personalized decisions. -- Avoid collecting personally identifiable information. -- Ask clarifying questions when the user intent or constraints (budget, risk tolerance, time horizon) are unclear.`; +1. Financial education: Explain investing concepts, terminology, and strategies in simple, beginner-friendly language. +2. General market insights: Discuss historical trends, market principles, and how different asset classes typically behave. +3. Investment fundamentals: Teach about diversification, risk management, dollar-cost averaging, and long-term investing principles. +4. Personal finance guidance: Help with budgeting basics, emergency funds, debt management, and retirement planning concepts. + +Response style: +Write in a natural, conversational tone as if you're chatting with a friend over coffee. Be encouraging and educational rather than giving specific investment advice. Use analogies and everyday examples to explain complex concepts in a way that feels relatable. Always focus on teaching principles rather than recommending specific investments. Be honest about not having access to real-time market data, and naturally encourage users to do their own research and consult professionals for personalized advice. Avoid using bullet points or formal lists - instead, weave information into flowing, natural sentences that feel like genuine conversation. Keep your responses short and concise, around 150-200 words maximum. + +Important disclaimers: +Remember to naturally work into your conversations that you're an educational AI guide, not a licensed financial advisor. You don't have access to real-time market data or current prices. All the information you share is for educational purposes only, not personalized financial advice. Always encourage users to consult with qualified financial professionals for actual investment decisions. Naturally remind them that past performance never guarantees future results and all investments carry risk, including potential loss of principal. + +When discussing investments: +Focus on general principles and educational concepts while explaining both potential benefits and risks in a conversational way. Naturally emphasize the importance of diversification and long-term thinking. Gently remind users to only invest what they can afford to lose and suggest they research thoroughly while considering their personal financial situation. Make these important points feel like natural parts of the conversation rather than formal warnings.`; } // Store conversation history for each session @@ -125,9 +108,6 @@ app.post("/ask", async (req: Request, res: Response) => { const customProperties = { agentid: NVM_AGENT_ID, sessionid: sessionId, - credit_amount: "1", - credit_usd_rate: "0.001", - credit_price_usd: "0.001", operation: "financial_advice", }; @@ -143,7 +123,7 @@ app.post("/ask", async (req: Request, res: Response) => { model: "gpt-4o-mini", messages: messages, temperature: 0.3, - max_tokens: 1000, + max_tokens: 250, }); // Extract the AI's response @@ -153,13 +133,18 @@ app.post("/ask", async (req: Request, res: Response) => { messages.push({ role: "assistant", content: response }); sessions.set(sessionId, messages); - // Redeem credits after successful API call + // Initialize redemption result let redemptionResult: any; + + // Define the amount of credits to redeem for this request + const credit_amount = 1; + + // Redeem credits after successful API call try { redemptionResult = await payments.requests.redeemCreditsFromRequest( agentRequest.agentRequestId, requestAccessToken, - 1n + BigInt(credit_amount) ); redemptionResult.creditsRedeemed = 1; } catch (redeemErr) { @@ -186,6 +171,7 @@ app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok" }); }); +// Start the server app.listen(PORT, () => { console.log(`Financial Agent listening on http://localhost:${PORT}`); }); diff --git a/financial-agent/agent/index_unprotected.ts b/financial-agent/agent/index_unprotected.ts index 5de2551..5e0d01d 100644 --- a/financial-agent/agent/index_unprotected.ts +++ b/financial-agent/agent/index_unprotected.ts @@ -23,40 +23,23 @@ const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); // Define the AI assistant's role and behavior function getSystemPrompt(): string { - return `You are FinGuide, a professional financial advisor and market analyst specializing in cryptocurrency and traditional markets. + return `You are FinGuide, a friendly financial education AI designed to help people learn about investing, personal finance, and market concepts. + Your role is to provide: -1. Real-time market data: current prices of cryptocurrencies, stock market performance, and key market indicators. -2. Investment analysis: monthly returns of major companies, market trends, and investment opportunities. -3. Financial advice: recommendations on whether to invest in specific assets based on current market conditions in a generic way. -4. Educational content: explain financial concepts, market dynamics, and investment strategies in simple terms. - -Response requirements: -- Be accurate and rely on current market data where possible. -- Include specific numbers and percentages when relevant. -- Provide balanced advice considering both opportunities and risks. -- Be educational and explain the reasoning behind recommendations. -- Always include appropriate risk warnings. -- Maintain a professional but accessible tone for both beginners and experienced investors. - -When providing investment advice: -- Consider the user's risk tolerance (no need to ask if not specified). -- Mention both potential gains and potential losses. -- Include time horizon recommendations. -- Suggest diversification strategies. -- Always remind: past performance does not guarantee future results. - -Formatting: -- Use clear headings and bullet points. -- Display current market data prominently. -- Highlight risk warnings in bold. -- Provide actionable recommendations when appropriate. - -Important constraints: -- You provide financial information and general advice, not personalized financial planning. -- Recommend consulting with a qualified financial advisor for personalized decisions. -- Avoid collecting personally identifiable information. -- Ask clarifying questions when the user intent or constraints (budget, risk tolerance, time horizon) are unclear.`; +1. Financial education: Explain investing concepts, terminology, and strategies in simple, beginner-friendly language. +2. General market insights: Discuss historical trends, market principles, and how different asset classes typically behave. +3. Investment fundamentals: Teach about diversification, risk management, dollar-cost averaging, and long-term investing principles. +4. Personal finance guidance: Help with budgeting basics, emergency funds, debt management, and retirement planning concepts. + +Response style: +Write in a natural, conversational tone as if you're chatting with a friend over coffee. Be encouraging and educational rather than giving specific investment advice. Use analogies and everyday examples to explain complex concepts in a way that feels relatable. Always focus on teaching principles rather than recommending specific investments. Be honest about not having access to real-time market data, and naturally encourage users to do their own research and consult professionals for personalized advice. Avoid using bullet points or formal lists - instead, weave information into flowing, natural sentences that feel like genuine conversation. Keep your responses short and concise, around 150-200 words maximum. + +Important disclaimers: +Remember to naturally work into your conversations that you're an educational AI guide, not a licensed financial advisor. You don't have access to real-time market data or current prices. All the information you share is for educational purposes only, not personalized financial advice. Always encourage users to consult with qualified financial professionals for actual investment decisions. Naturally remind them that past performance never guarantees future results and all investments carry risk, including potential loss of principal. + +When discussing investments: +Focus on general principles and educational concepts while explaining both potential benefits and risks in a conversational way. Naturally emphasize the importance of diversification and long-term thinking. Gently remind users to only invest what they can afford to lose and suggest they research thoroughly while considering their personal financial situation. Make these important points feel like natural parts of the conversation rather than formal warnings.`; } // Store conversation history for each session @@ -92,7 +75,7 @@ app.post("/ask", async (req: Request, res: Response) => { model: "gpt-4o-mini", messages: messages, temperature: 0.3, - max_tokens: 1000, + max_tokens: 250, }); // Extract the AI's response @@ -115,6 +98,7 @@ app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok" }); }); +// Start the server app.listen(PORT, () => { console.log(`Financial Agent listening on http://localhost:${PORT}`); }); diff --git a/financial-agent/client/index_nevermined.ts b/financial-agent/client/index_nevermined.ts index b9aa1ef..7f8c9a1 100644 --- a/financial-agent/client/index_nevermined.ts +++ b/financial-agent/client/index_nevermined.ts @@ -12,11 +12,11 @@ const AGENT_ID = process.env.NVM_AGENT_ID as string; const SUBSCRIBER_API_KEY = process.env.SUBSCRIBER_NVM_API_KEY as string; const NVM_ENVIRONMENT = (process.env.NVM_ENV || "sandbox") as EnvironmentName; -// Define test questions to demonstrate conversation continuity -const TEST_QUESTIONS = [ - "What is your market outlook for Bitcoin over the next month?", - "How are major stock indices performing today and what trends are notable?", - "What risks should I consider before increasing exposure to tech stocks?", +// Define demo conversation to show chatbot-style interaction +const DEMO_CONVERSATION_QUESTIONS = [ + "Hi there! I'm new to investing and keep hearing about diversification. Can you explain what that means in simple terms?", + "That makes sense! So if I want to start investing but only have $100 a month, what should I focus on first?", + "I've been thinking about cryptocurrency. What should a beginner like me know before investing in crypto?", ]; // Validate required environment variables @@ -30,7 +30,7 @@ function validateEnvironment(): void { } // Get or purchase access token for protected agent -async function getAccessToken(): Promise { +async function getorPurchaseAccessToken(): Promise { console.log("🔐 Setting up Nevermined access..."); // Initialize Nevermined Payments SDK @@ -61,38 +61,61 @@ async function getAccessToken(): Promise { return credentials.accessToken; } -// Send a question to the protected financial agent -async function askAgent(input: string, accessToken: string, sessionId?: string): Promise<{ output: string; sessionId: string; redemptionResult?: any }> { - // Prepare request payload - const requestBody = { - input_query: input, - sessionId: sessionId - }; - - // Prepare headers with authorization - const headers = { - "Content-Type": "application/json", - "Authorization": `Bearer ${accessToken}` +// Simple loading animation for terminal +function startLoadingAnimation(): () => void { + const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + let i = 0; + const interval = setInterval(() => { + process.stdout.write(`\r${frames[i]} FinGuide is thinking...`); + i = (i + 1) % frames.length; + }, 100); + + return () => { + clearInterval(interval); + process.stdout.write('\r'); }; +} - // Make HTTP request to protected agent - const response = await fetch(`${AGENT_URL}/ask`, { - method: "POST", - headers: headers, - body: JSON.stringify(requestBody), - }); - - // Handle HTTP errors (including payment required) - if (!response.ok) { - const errorText = await response.text().catch(() => ""); - if (response.status === 402) { - throw new Error("Payment Required - insufficient credits or subscription"); +// Send a question to the protected financial agent +async function askAgent(input: string, accessToken: string, sessionId?: string): Promise<{ output: string; sessionId: string; redemptionResult?: any }> { + // Start loading animation + const stopLoading = startLoadingAnimation(); + + try { + // Prepare request payload + const requestBody = { + input_query: input, + sessionId: sessionId + }; + + // Prepare headers with authorization + const headers = { + "Content-Type": "application/json", + "Authorization": `Bearer ${accessToken}` + }; + + // Make HTTP request to protected agent + const response = await fetch(`${AGENT_URL}/ask`, { + method: "POST", + headers: headers, + body: JSON.stringify(requestBody), + }); + + // Handle HTTP errors (including payment required) + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + if (response.status === 402) { + throw new Error("Payment Required - insufficient credits or subscription"); + } + throw new Error(`Agent request failed: ${response.status} ${response.statusText} ${errorText}`); } - throw new Error(`Agent request failed: ${response.status} ${response.statusText} ${errorText}`); - } - // Parse and return JSON response - return await response.json() as { output: string; sessionId: string; redemptionResult?: any }; + // Parse and return JSON response + return await response.json() as { output: string; sessionId: string; redemptionResult?: any }; + } finally { + // Stop loading animation + stopLoading(); + } } /** @@ -107,14 +130,14 @@ async function runDemo(): Promise { validateEnvironment(); // Obtain access token for protected agent - const accessToken = await getAccessToken(); + const accessToken = await getorPurchaseAccessToken(); // Track session across multiple questions let sessionId: string | undefined; - // Send each test question and maintain conversation context - for (let i = 0; i < TEST_QUESTIONS.length; i++) { - const question = TEST_QUESTIONS[i]; + // Send each demo question and maintain conversation context + for (let i = 0; i < DEMO_CONVERSATION_QUESTIONS.length; i++) { + const question = DEMO_CONVERSATION_QUESTIONS[i]; console.log(`📝 Question ${i + 1}: ${question}`); diff --git a/financial-agent/client/index_unprotected.ts b/financial-agent/client/index_unprotected.ts index 6a6faea..1a6d0eb 100644 --- a/financial-agent/client/index_unprotected.ts +++ b/financial-agent/client/index_unprotected.ts @@ -7,36 +7,59 @@ import "dotenv/config"; // Configuration: Agent URL from environment or default const AGENT_URL = process.env.AGENT_URL || "http://localhost:3001"; -// Define test questions to demonstrate conversation continuity -const TEST_QUESTIONS = [ - "What is your market outlook for Bitcoin over the next month?", - "How are major stock indices performing today and what trends are notable?", - "What risks should I consider before increasing exposure to tech stocks?", +// Define demo conversation to show chatbot-style interaction +const DEMO_CONVERSATION_QUESTIONS = [ + "Hi there! I'm new to investing and keep hearing about diversification. Can you explain what that means in simple terms?", + "That makes sense! So if I want to start investing but only have $100 a month, what should I focus on first?", + "I've been thinking about cryptocurrency. What should a beginner like me know before investing in crypto?", ]; +// Simple loading animation for terminal +function startLoadingAnimation(): () => void { + const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + let i = 0; + const interval = setInterval(() => { + process.stdout.write(`\r${frames[i]} FinGuide is thinking...`); + i = (i + 1) % frames.length; + }, 100); + + return () => { + clearInterval(interval); + process.stdout.write('\r'); + }; +} + // Send a question to the financial agent async function askAgent(input: string, sessionId?: string): Promise<{ output: string; sessionId: string }> { - // Prepare request payload - const requestBody = { - input_query: input, - sessionId: sessionId - }; + // Start loading animation + const stopLoading = startLoadingAnimation(); + + try { + // Prepare request payload + const requestBody = { + input_query: input, + sessionId: sessionId + }; + + // Make HTTP request to agent + const response = await fetch(`${AGENT_URL}/ask`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + + // Handle HTTP errors + if (!response.ok) { + const errorText = await response.text().catch(() => ""); + throw new Error(`Agent request failed: ${response.status} ${response.statusText} ${errorText}`); + } - // Make HTTP request to agent - const response = await fetch(`${AGENT_URL}/ask`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - - // Handle HTTP errors - if (!response.ok) { - const errorText = await response.text().catch(() => ""); - throw new Error(`Agent request failed: ${response.status} ${response.statusText} ${errorText}`); + // Parse and return JSON response + return await response.json() as { output: string; sessionId: string }; + } finally { + // Stop loading animation + stopLoading(); } - - // Parse and return JSON response - return await response.json() as { output: string; sessionId: string }; } /** @@ -50,9 +73,9 @@ async function runDemo(): Promise { // Track session across multiple questions let sessionId: string | undefined; - // Send each test question and maintain conversation context - for (let i = 0; i < TEST_QUESTIONS.length; i++) { - const question = TEST_QUESTIONS[i]; + // Send each demo question and maintain conversation context + for (let i = 0; i < DEMO_CONVERSATION_QUESTIONS.length; i++) { + const question = DEMO_CONVERSATION_QUESTIONS[i]; console.log(`📝 Question ${i + 1}: ${question}`); From 7c2aa064b7289d42c24fb22e5bd9a68e41868000 Mon Sep 17 00:00:00 2001 From: nomeforme <143119811+nomeforme@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:50:20 +0200 Subject: [PATCH 4/6] simple dynamic credits a 10 * (completion tokens / max tokens) --- financial-agent/agent/index_nevermined.ts | 57 +++++++++++++++++----- financial-agent/agent/index_unprotected.ts | 31 +++++++++--- 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/financial-agent/agent/index_nevermined.ts b/financial-agent/agent/index_nevermined.ts index 31f503c..ec30b0a 100644 --- a/financial-agent/agent/index_nevermined.ts +++ b/financial-agent/agent/index_nevermined.ts @@ -34,8 +34,8 @@ const payments = Payments.getInstance({ environment: NVM_ENV, }); -// Define the AI assistant's role and behavior -function getSystemPrompt(): string { +// Define the AI's role and behavior +function getSystemPrompt(maxTokens: number): string { return `You are FinGuide, a friendly financial education AI designed to help people learn about investing, personal finance, and market concepts. Your role is to provide: @@ -46,13 +46,40 @@ Your role is to provide: 4. Personal finance guidance: Help with budgeting basics, emergency funds, debt management, and retirement planning concepts. Response style: -Write in a natural, conversational tone as if you're chatting with a friend over coffee. Be encouraging and educational rather than giving specific investment advice. Use analogies and everyday examples to explain complex concepts in a way that feels relatable. Always focus on teaching principles rather than recommending specific investments. Be honest about not having access to real-time market data, and naturally encourage users to do their own research and consult professionals for personalized advice. Avoid using bullet points or formal lists - instead, weave information into flowing, natural sentences that feel like genuine conversation. Keep your responses short and concise, around 150-200 words maximum. +Write in a natural, conversational tone as if you're chatting with a friend over coffee. Be encouraging and educational rather +than giving specific investment advice. Use analogies and everyday examples to explain complex concepts in a way that feels +relatable. Always focus on teaching principles rather than recommending specific investments. Be honest about not having access +to real-time market data, and naturally encourage users to do their own research and consult professionals for personalized +advice. Avoid using bullet points or formal lists - instead, weave information into flowing, natural sentences that feel +like genuine conversation. Adjust your response length based on the complexity of the question - for simple questions, +keep responses concise (50-100 words), but for complex topics that need thorough explanation, feel free to use +up to ${maxTokens} tokens to provide comprehensive educational value. Important disclaimers: -Remember to naturally work into your conversations that you're an educational AI guide, not a licensed financial advisor. You don't have access to real-time market data or current prices. All the information you share is for educational purposes only, not personalized financial advice. Always encourage users to consult with qualified financial professionals for actual investment decisions. Naturally remind them that past performance never guarantees future results and all investments carry risk, including potential loss of principal. +Remember to naturally work into your conversations that you're an educational AI guide, not a licensed financial advisor. +You don't have access to real-time market data or current prices. All the information you share is for educational purposes only, +not personalized financial advice. Always encourage users to consult with qualified financial professionals for actual +investment decisions. Naturally remind them that past performance never guarantees future results and all investments +carry risk, including potential loss of principal. When discussing investments: -Focus on general principles and educational concepts while explaining both potential benefits and risks in a conversational way. Naturally emphasize the importance of diversification and long-term thinking. Gently remind users to only invest what they can afford to lose and suggest they research thoroughly while considering their personal financial situation. Make these important points feel like natural parts of the conversation rather than formal warnings.`; +Focus on general principles and educational concepts while explaining both potential benefits and risks in a conversational way. +Naturally emphasize the importance of diversification and long-term thinking. Gently remind users to only invest what they can +afford to lose and suggest they research thoroughly while considering their personal financial situation. +Make these important points feel like natural parts of the conversation rather than formal warnings.`; +} + +// Calculate dynamic credit amount based on token usage +function calculateCreditAmount(tokensUsed: number, maxTokens: number): number { + // Formula: 10 * (actual_tokens / max_tokens) + // This rewards shorter responses with lower costs + const tokenUtilization = Math.min(tokensUsed / maxTokens, 1); // Cap at 1 + const baseCreditAmount = 10 * tokenUtilization; + const creditAmount = Math.max(Math.ceil(baseCreditAmount), 1); // Minimum 1 credit + + console.log(`Token usage: ${tokensUsed}/${maxTokens} (${(tokenUtilization * 100).toFixed(1)}%) - Credits: ${creditAmount}`); + + return creditAmount; } // Store conversation history for each session @@ -90,6 +117,9 @@ app.post("/ask", async (req: Request, res: Response) => { let { sessionId } = req.body as { sessionId?: string }; if (!sessionId) sessionId = crypto.randomUUID(); + // Define the maximum number of tokens for the completion response + const maxTokens = 250; + // Retrieve existing conversation history or start fresh let messages = sessions.get(sessionId) || []; @@ -97,7 +127,7 @@ app.post("/ask", async (req: Request, res: Response) => { if (messages.length === 0) { messages.push({ role: "system", - content: getSystemPrompt() + content: getSystemPrompt(maxTokens) }); } @@ -123,30 +153,31 @@ app.post("/ask", async (req: Request, res: Response) => { model: "gpt-4o-mini", messages: messages, temperature: 0.3, - max_tokens: 250, + max_tokens: maxTokens, }); - // Extract the AI's response + // Extract the AI's response and token usage const response = completion.choices[0]?.message?.content || "No response generated"; + const tokensUsed = completion.usage?.completion_tokens || 0; // Save the AI's response to conversation history messages.push({ role: "assistant", content: response }); sessions.set(sessionId, messages); + // Calculate dynamic credit amount based on token usage + const creditAmount = calculateCreditAmount(tokensUsed, maxTokens); + // Initialize redemption result let redemptionResult: any; - // Define the amount of credits to redeem for this request - const credit_amount = 1; - // Redeem credits after successful API call try { redemptionResult = await payments.requests.redeemCreditsFromRequest( agentRequest.agentRequestId, requestAccessToken, - BigInt(credit_amount) + BigInt(creditAmount) ); - redemptionResult.creditsRedeemed = 1; + redemptionResult.creditsRedeemed = creditAmount; } catch (redeemErr) { console.error("Failed to redeem credits:", redeemErr); redemptionResult = { diff --git a/financial-agent/agent/index_unprotected.ts b/financial-agent/agent/index_unprotected.ts index 5e0d01d..25843b0 100644 --- a/financial-agent/agent/index_unprotected.ts +++ b/financial-agent/agent/index_unprotected.ts @@ -21,8 +21,8 @@ if (!OPENAI_API_KEY) { // Initialize OpenAI client with API key const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); -// Define the AI assistant's role and behavior -function getSystemPrompt(): string { +// Define the AI's role and behavior +function getSystemPrompt(maxTokens: number): string { return `You are FinGuide, a friendly financial education AI designed to help people learn about investing, personal finance, and market concepts. Your role is to provide: @@ -33,13 +33,27 @@ Your role is to provide: 4. Personal finance guidance: Help with budgeting basics, emergency funds, debt management, and retirement planning concepts. Response style: -Write in a natural, conversational tone as if you're chatting with a friend over coffee. Be encouraging and educational rather than giving specific investment advice. Use analogies and everyday examples to explain complex concepts in a way that feels relatable. Always focus on teaching principles rather than recommending specific investments. Be honest about not having access to real-time market data, and naturally encourage users to do their own research and consult professionals for personalized advice. Avoid using bullet points or formal lists - instead, weave information into flowing, natural sentences that feel like genuine conversation. Keep your responses short and concise, around 150-200 words maximum. +Write in a natural, conversational tone as if you're chatting with a friend over coffee. Be encouraging and educational rather +than giving specific investment advice. Use analogies and everyday examples to explain complex concepts in a way that feels +relatable. Always focus on teaching principles rather than recommending specific investments. Be honest about not having access +to real-time market data, and naturally encourage users to do their own research and consult professionals for personalized +advice. Avoid using bullet points or formal lists - instead, weave information into flowing, natural sentences that feel +like genuine conversation. Adjust your response length based on the complexity of the question - for simple questions, +keep responses concise (50-100 words), but for complex topics that need thorough explanation, feel free to use +up to ${maxTokens} tokens to provide comprehensive educational value. Important disclaimers: -Remember to naturally work into your conversations that you're an educational AI guide, not a licensed financial advisor. You don't have access to real-time market data or current prices. All the information you share is for educational purposes only, not personalized financial advice. Always encourage users to consult with qualified financial professionals for actual investment decisions. Naturally remind them that past performance never guarantees future results and all investments carry risk, including potential loss of principal. +Remember to naturally work into your conversations that you're an educational AI guide, not a licensed financial advisor. +You don't have access to real-time market data or current prices. All the information you share is for educational purposes only, +not personalized financial advice. Always encourage users to consult with qualified financial professionals for actual +investment decisions. Naturally remind them that past performance never guarantees future results and all investments +carry risk, including potential loss of principal. When discussing investments: -Focus on general principles and educational concepts while explaining both potential benefits and risks in a conversational way. Naturally emphasize the importance of diversification and long-term thinking. Gently remind users to only invest what they can afford to lose and suggest they research thoroughly while considering their personal financial situation. Make these important points feel like natural parts of the conversation rather than formal warnings.`; +Focus on general principles and educational concepts while explaining both potential benefits and risks in a conversational way. +Naturally emphasize the importance of diversification and long-term thinking. Gently remind users to only invest what they can +afford to lose and suggest they research thoroughly while considering their personal financial situation. +Make these important points feel like natural parts of the conversation rather than formal warnings.`; } // Store conversation history for each session @@ -56,6 +70,9 @@ app.post("/ask", async (req: Request, res: Response) => { let { sessionId } = req.body as { sessionId?: string }; if (!sessionId) sessionId = crypto.randomUUID(); + // Define the maximum number of tokens for the completion response + const maxTokens = 250; + // Retrieve existing conversation history or start fresh let messages = sessions.get(sessionId) || []; @@ -63,7 +80,7 @@ app.post("/ask", async (req: Request, res: Response) => { if (messages.length === 0) { messages.push({ role: "system", - content: getSystemPrompt() + content: getSystemPrompt(maxTokens) }); } @@ -75,7 +92,7 @@ app.post("/ask", async (req: Request, res: Response) => { model: "gpt-4o-mini", messages: messages, temperature: 0.3, - max_tokens: 250, + max_tokens: maxTokens, }); // Extract the AI's response From fa054151b2f5aeef7b8fa1d7db41fea54af4f351 Mon Sep 17 00:00:00 2001 From: nomeforme <143119811+nomeforme@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:11:20 +0200 Subject: [PATCH 5/6] commit --- financial-agent/client/index_nevermined.ts | 102 --------------------- 1 file changed, 102 deletions(-) diff --git a/financial-agent/client/index_nevermined.ts b/financial-agent/client/index_nevermined.ts index 6e240c8..7f8c9a1 100644 --- a/financial-agent/client/index_nevermined.ts +++ b/financial-agent/client/index_nevermined.ts @@ -123,17 +123,8 @@ async function askAgent(input: string, accessToken: string, sessionId?: string): * Sends predefined financial questions to the agent with Authorization and reuses sessionId to preserve context. * @returns {Promise} Resolves when the run finishes */ -<<<<<<< HEAD async function runDemo(): Promise { console.log("🚀 Starting Financial Agent Demo (Protected with Nevermined)\n"); -======= -async function main(): Promise { - const PORT = process.env.PORT ? Number(process.env.PORT) : 3001; - if (!PORT) { - throw new Error("PORT is required in client env"); - } - const baseUrl = process.env.AGENT_URL || `http://localhost:${PORT}`; ->>>>>>> main // Validate environment setup validateEnvironment(); @@ -144,28 +135,9 @@ async function main(): Promise { // Track session across multiple questions let sessionId: string | undefined; -<<<<<<< HEAD // Send each demo question and maintain conversation context for (let i = 0; i < DEMO_CONVERSATION_QUESTIONS.length; i++) { const question = DEMO_CONVERSATION_QUESTIONS[i]; -======= - const planId = process.env.NVM_PLAN_ID as string; - const agentId = process.env.NVM_AGENT_ID as string; - const nvmApiKey = process.env.SUBSCRIBER_NVM_API_KEY as string; - const nvmEnv = (process.env.NVM_ENVIRONMENT || "sandbox") as EnvironmentName; - if (!planId || !agentId) { - throw new Error("NVM_PLAN_ID and NVM_AGENT_ID are required in client env"); - } - if (!nvmApiKey) { - throw new Error("SUBSCRIBER_NVM_API_KEY is required in client env"); - } - bearer = await getOrBuyAccessToken({ - planId, - agentId, - nvmApiKey, - nvmEnv, - }); ->>>>>>> main console.log(`📝 Question ${i + 1}: ${question}`); @@ -195,82 +167,8 @@ async function main(): Promise { console.log("✅ Demo completed!"); } -<<<<<<< HEAD // Run the demo and handle any errors runDemo().catch((error) => { console.error("💥 Demo failed:", error); -======= -/** - * Perform a POST /ask to the protected agent. - * @param {string} baseUrl - Base URL of the agent service - * @param {string} input - User question text - * @param {string} [sessionId] - Optional existing session id - * @param {string} [bearer] - Authorization token to access protected endpoint - * @returns {Promise<{ output: string; sessionId: string }>} The JSON response containing output and sessionId - */ -async function askAgent( - baseUrl: string, - input: string, - sessionId?: string, - bearer?: string -): Promise<{ output: string; sessionId: string }> { - const res = await fetch(`${baseUrl}/ask`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), - }, - body: JSON.stringify({ input_query: input, sessionId }), - }); - if (!res.ok) { - const errorText = await res.text().catch(() => ""); - throw new Error( - `Agent request failed: ${res.status} ${res.statusText} ${errorText}` - ); - } - const data = (await res.json()) as { output: string; sessionId: string }; - return data; -} - -/** - * Get a valid access token by checking plan balance/subscription first. - * If not subscribed or no credits, purchase the plan and then fetch the token. - * @param {Object} opts - Options object - * @param {string} opts.planId - Plan identifier - * @param {string} opts.agentId - Agent identifier - * @param {string} opts.nvmApiKey - Nevermined API Key (subscriber) - * @param {EnvironmentName} opts.nvmEnv - Nevermined environmentment - * @returns {Promise} The access token string - */ -async function getOrBuyAccessToken(opts: { - planId: string; - agentId: string; - nvmApiKey: string; - nvmEnv: EnvironmentName; -}): Promise { - const payments = Payments.getInstance({ - nvmApiKey: opts.nvmApiKey, - environment: opts.nvmEnv, - }); - const balanceInfo: any = await payments.plans.getPlanBalance(opts.planId); - const hasCredits = Number(balanceInfo?.balance ?? 0) > 0; - const isSubscriber = balanceInfo?.isSubscriber === true; - if (!isSubscriber && !hasCredits) { - console.log("Ordering plan with key: ", opts.nvmApiKey); - await payments.plans.orderPlan(opts.planId); - } - const creds = await payments.agents.getAgentAccessToken( - opts.planId, - opts.agentId - ); - if (!creds?.accessToken) throw new Error("Access token unavailable"); - return creds.accessToken; -} - -// Run the client -main().catch((err) => { - // eslint-disable-next-line no-console - console.error("[CLIENT] Error:", err); ->>>>>>> main process.exit(1); }); From a24f4a6752a0c986ca5d19cac8eeb69370b4ce18 Mon Sep 17 00:00:00 2001 From: nomeforme <143119811+nomeforme@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:14:39 +0200 Subject: [PATCH 6/6] commit --- a2a-examples/observability-a2a-agent | 1 + agent-examples/observability-gpt-agent | 1 + financial-agent/agent/index_nevermined.ts | 3 +-- financial-agent/client/index_nevermined.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 160000 a2a-examples/observability-a2a-agent create mode 160000 agent-examples/observability-gpt-agent diff --git a/a2a-examples/observability-a2a-agent b/a2a-examples/observability-a2a-agent new file mode 160000 index 0000000..88ad367 --- /dev/null +++ b/a2a-examples/observability-a2a-agent @@ -0,0 +1 @@ +Subproject commit 88ad367854c1027b55bcbe628c9f40df8ca4f6d5 diff --git a/agent-examples/observability-gpt-agent b/agent-examples/observability-gpt-agent new file mode 160000 index 0000000..8451d83 --- /dev/null +++ b/agent-examples/observability-gpt-agent @@ -0,0 +1 @@ +Subproject commit 8451d83316e9aa7cffc4a4a1b0ae305bca8ad704 diff --git a/financial-agent/agent/index_nevermined.ts b/financial-agent/agent/index_nevermined.ts index 771d0aa..2d8e2e5 100644 --- a/financial-agent/agent/index_nevermined.ts +++ b/financial-agent/agent/index_nevermined.ts @@ -14,8 +14,7 @@ app.use(express.json()); const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; const NVM_API_KEY = process.env.BUILDER_NVM_API_KEY ?? ""; -const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || - "sandbox") as EnvironmentName; +const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || "staging_sandbox") as EnvironmentName; const NVM_AGENT_ID = process.env.NVM_AGENT_ID ?? ""; const NVM_AGENT_HOST = process.env.NVM_AGENT_HOST || `http://localhost:${PORT}`; diff --git a/financial-agent/client/index_nevermined.ts b/financial-agent/client/index_nevermined.ts index 7f8c9a1..67016c6 100644 --- a/financial-agent/client/index_nevermined.ts +++ b/financial-agent/client/index_nevermined.ts @@ -10,7 +10,7 @@ const AGENT_URL = process.env.AGENT_URL || "http://localhost:3000"; const PLAN_ID = process.env.NVM_PLAN_ID as string; const AGENT_ID = process.env.NVM_AGENT_ID as string; const SUBSCRIBER_API_KEY = process.env.SUBSCRIBER_NVM_API_KEY as string; -const NVM_ENVIRONMENT = (process.env.NVM_ENV || "sandbox") as EnvironmentName; +const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || "staging_sandbox") as EnvironmentName; // Define demo conversation to show chatbot-style interaction const DEMO_CONVERSATION_QUESTIONS = [