From 2af00e9268f98dc743e4b54a73fe99beb8e7d5a2 Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 11:42:58 +0100 Subject: [PATCH 01/25] Add initial node mcp setup --- src/mcp/movie-reviews-mcp/.gitignore | 1 + src/mcp/movie-reviews-mcp/package-lock.json | 1103 +++++++++++++++++++ src/mcp/movie-reviews-mcp/package.json | 21 + src/mcp/movie-reviews-mcp/src/index.ts | 0 4 files changed, 1125 insertions(+) create mode 100644 src/mcp/movie-reviews-mcp/.gitignore create mode 100644 src/mcp/movie-reviews-mcp/package-lock.json create mode 100644 src/mcp/movie-reviews-mcp/package.json create mode 100644 src/mcp/movie-reviews-mcp/src/index.ts diff --git a/src/mcp/movie-reviews-mcp/.gitignore b/src/mcp/movie-reviews-mcp/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/package-lock.json b/src/mcp/movie-reviews-mcp/package-lock.json new file mode 100644 index 0000000..2df4554 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/package-lock.json @@ -0,0 +1,1103 @@ +{ + "name": "movie-reviews-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "movie-reviews-mcp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", + "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/src/mcp/movie-reviews-mcp/package.json b/src/mcp/movie-reviews-mcp/package.json new file mode 100644 index 0000000..d39805a --- /dev/null +++ b/src/mcp/movie-reviews-mcp/package.json @@ -0,0 +1,21 @@ +{ + "name": "movie-reviews-mcp", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "typescript": "^5.9.2" + } +} diff --git a/src/mcp/movie-reviews-mcp/src/index.ts b/src/mcp/movie-reviews-mcp/src/index.ts new file mode 100644 index 0000000..e69de29 From f43a1434ec268565255701be23c5c8805a591a4a Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:53:58 +0100 Subject: [PATCH 02/25] Models for cinema-mcp --- demo/demo_template 1.pptx | Bin 2153244 -> 2153430 bytes src/mcp/cinema-mcp/models.py | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/mcp/cinema-mcp/models.py diff --git a/demo/demo_template 1.pptx b/demo/demo_template 1.pptx index b6c2514971ff0d849f6c94363d6f9fa00abb679f..1732a44fbb859134bb92e70e6d14f0d741ae3d9a 100644 GIT binary patch delta 34280 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 diff --git a/src/mcp/cinema-mcp/models.py b/src/mcp/cinema-mcp/models.py new file mode 100644 index 0000000..bb7e181 --- /dev/null +++ b/src/mcp/cinema-mcp/models.py @@ -0,0 +1,73 @@ +""" +Cinema data models - Data classes for movies and reservations. +""" + +from dataclasses import dataclass +from typing import Optional, List +from datetime import datetime, date, time + +@dataclass +class MovieShowing: + """Represents a movie showing in the cinema""" + title: str + description: str + date: date # Date of the showing + time: time # Time of the showing + room: str # Cinema room/hall identifier + seats_available: int # Total seats available for this showing + seats_booked: int = 0 # Number of seats already booked + duration_minutes: Optional[int] = None # Movie duration in minutes + genre: Optional[str] = None # Movie genre + rating: Optional[str] = None # Movie rating (PG, PG-13, R, etc.) + + @property + def seats_remaining(self) -> int: + """Calculate remaining available seats""" + return max(0, self.seats_available - self.seats_booked) + + @property + def is_sold_out(self) -> bool: + """Check if the movie showing is sold out""" + return self.seats_remaining == 0 + + @property + def occupancy_percentage(self) -> float: + """Calculate the occupancy percentage""" + if self.seats_available == 0: + return 0.0 + return (self.seats_booked / self.seats_available) * 100 + +@dataclass +class ContactInfo: + """Contact information for a reservation""" + name: str + email: str + phone: Optional[str] = None + +@dataclass +class MovieReservation: + """Represents a reservation for a movie showing""" + movie_title: str # Title of the movie (for easy reference) + movie_date: date # Date of the movie showing + movie_time: time # Time of the movie showing + room: str # Cinema room + seats_reserved: int # Number of seats reserved + contact_info: ContactInfo # Customer contact information + reservation_datetime: datetime # When the reservation was made + status: str = "confirmed" # Status: confirmed, cancelled, completed + special_requests: Optional[str] = None # Any special requests or notes + + @property + def is_active(self) -> bool: + """Check if the reservation is still active""" + return self.status == "confirmed" + + @property + def customer_name(self) -> str: + """Get customer name for easy access""" + return self.contact_info.name + + @property + def customer_email(self) -> str: + """Get customer email for easy access""" + return self.contact_info.email From f0eacac56466be48d7d23d2751511becf0766822 Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 12:01:08 +0100 Subject: [PATCH 03/25] Add tutorial code and start readme --- src/mcp/movie-reviews-mcp/package.json | 3 +- src/mcp/movie-reviews-mcp/readme.md | 16 ++ src/mcp/movie-reviews-mcp/src/index.ts | 231 ++++++++++++++++++++++++ src/mcp/movie-reviews-mcp/tsconfig.json | 15 ++ 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 src/mcp/movie-reviews-mcp/readme.md create mode 100644 src/mcp/movie-reviews-mcp/tsconfig.json diff --git a/src/mcp/movie-reviews-mcp/package.json b/src/mcp/movie-reviews-mcp/package.json index d39805a..d695836 100644 --- a/src/mcp/movie-reviews-mcp/package.json +++ b/src/mcp/movie-reviews-mcp/package.json @@ -4,7 +4,8 @@ "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + + "build": "tsc && chmod 755 build/index.js" }, "keywords": [], "author": "", diff --git a/src/mcp/movie-reviews-mcp/readme.md b/src/mcp/movie-reviews-mcp/readme.md new file mode 100644 index 0000000..7c8a74a --- /dev/null +++ b/src/mcp/movie-reviews-mcp/readme.md @@ -0,0 +1,16 @@ + +## Setup + +1. Install Claud Desktop: https://claude.ai/download +2. Add `claude_desktop_config.json` file in install folder (likely `C:\Users\\AppData\Local\AnthropicClaude`) with the following contents (replace folder location): + +```json +{ + "mcpServers": { + "weather": { + "command": "node", + "args": ["C:\\\\Hackathon-2025\\src\\mcp\\movie-reviews-mcp\build\\index.js"] + } + } +} +``` diff --git a/src/mcp/movie-reviews-mcp/src/index.ts b/src/mcp/movie-reviews-mcp/src/index.ts index e69de29..5860904 100644 --- a/src/mcp/movie-reviews-mcp/src/index.ts +++ b/src/mcp/movie-reviews-mcp/src/index.ts @@ -0,0 +1,231 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +const NWS_API_BASE = "https://api.weather.gov"; +const USER_AGENT = "weather-app/1.0"; + +// Create server instance +const server = new McpServer({ + name: "weather", + version: "1.0.0", + capabilities: { + resources: {}, + tools: {}, + }, +}); + +// Helper function for making NWS API requests +async function makeNWSRequest(url: string): Promise { + const headers = { + "User-Agent": USER_AGENT, + Accept: "application/geo+json", + }; + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return (await response.json()) as T; + } catch (error) { + console.error("Error making NWS request:", error); + return null; + } +} + +interface AlertFeature { + properties: { + event?: string; + areaDesc?: string; + severity?: string; + status?: string; + headline?: string; + }; +} + +// Format alert data +function formatAlert(feature: AlertFeature): string { + const props = feature.properties; + return [ + `Event: ${props.event || "Unknown"}`, + `Area: ${props.areaDesc || "Unknown"}`, + `Severity: ${props.severity || "Unknown"}`, + `Status: ${props.status || "Unknown"}`, + `Headline: ${props.headline || "No headline"}`, + "---", + ].join("\n"); +} + +interface ForecastPeriod { + name?: string; + temperature?: number; + temperatureUnit?: string; + windSpeed?: string; + windDirection?: string; + shortForecast?: string; +} + +interface AlertsResponse { + features: AlertFeature[]; +} + +interface PointsResponse { + properties: { + forecast?: string; + }; +} + +interface ForecastResponse { + properties: { + periods: ForecastPeriod[]; + }; +} + +// Register weather tools +server.tool( + "get_alerts", + "Get weather alerts for a state", + { + state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"), + }, + async ({ state }) => { + const stateCode = state.toUpperCase(); + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; + const alertsData = await makeNWSRequest(alertsUrl); + + if (!alertsData) { + return { + content: [ + { + type: "text", + text: "Failed to retrieve alerts data", + }, + ], + }; + } + + const features = alertsData.features || []; + if (features.length === 0) { + return { + content: [ + { + type: "text", + text: `No active alerts for ${stateCode}`, + }, + ], + }; + } + + const formattedAlerts = features.map(formatAlert); + const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`; + + return { + content: [ + { + type: "text", + text: alertsText, + }, + ], + }; + }, +); + +server.tool( + "get_forecast", + "Get weather forecast for a location", + { + latitude: z.number().min(-90).max(90).describe("Latitude of the location"), + longitude: z + .number() + .min(-180) + .max(180) + .describe("Longitude of the location"), + }, + async ({ latitude, longitude }) => { + // Get grid point data + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; + const pointsData = await makeNWSRequest(pointsUrl); + + if (!pointsData) { + return { + content: [ + { + type: "text", + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, + }, + ], + }; + } + + const forecastUrl = pointsData.properties?.forecast; + if (!forecastUrl) { + return { + content: [ + { + type: "text", + text: "Failed to get forecast URL from grid point data", + }, + ], + }; + } + + // Get forecast data + const forecastData = await makeNWSRequest(forecastUrl); + if (!forecastData) { + return { + content: [ + { + type: "text", + text: "Failed to retrieve forecast data", + }, + ], + }; + } + + const periods = forecastData.properties?.periods || []; + if (periods.length === 0) { + return { + content: [ + { + type: "text", + text: "No forecast periods available", + }, + ], + }; + } + + // Format forecast periods + const formattedForecast = periods.map((period: ForecastPeriod) => + [ + `${period.name || "Unknown"}:`, + `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`, + `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`, + `${period.shortForecast || "No forecast available"}`, + "---", + ].join("\n"), + ); + + const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`; + + return { + content: [ + { + type: "text", + text: forecastText, + }, + ], + }; + }, +); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Weather MCP Server running on stdio"); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); diff --git a/src/mcp/movie-reviews-mcp/tsconfig.json b/src/mcp/movie-reviews-mcp/tsconfig.json new file mode 100644 index 0000000..28933fb --- /dev/null +++ b/src/mcp/movie-reviews-mcp/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file From 7f52db363f979a5b205f8d665b6ad381c31320df Mon Sep 17 00:00:00 2001 From: Mateusz Wolochow Date: Wed, 24 Sep 2025 13:05:23 +0200 Subject: [PATCH 04/25] initial cinema mcp. --- src/mcp/cinema-mcp/.python-version | 1 + src/mcp/cinema-mcp/README.md | 0 src/mcp/cinema-mcp/main.py | 65 ++++ src/mcp/cinema-mcp/pyproject.toml | 10 + src/mcp/cinema-mcp/uv.lock | 525 +++++++++++++++++++++++++++++ 5 files changed, 601 insertions(+) create mode 100644 src/mcp/cinema-mcp/.python-version create mode 100644 src/mcp/cinema-mcp/README.md create mode 100644 src/mcp/cinema-mcp/main.py create mode 100644 src/mcp/cinema-mcp/pyproject.toml create mode 100644 src/mcp/cinema-mcp/uv.lock diff --git a/src/mcp/cinema-mcp/.python-version b/src/mcp/cinema-mcp/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/src/mcp/cinema-mcp/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/src/mcp/cinema-mcp/README.md b/src/mcp/cinema-mcp/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py new file mode 100644 index 0000000..2b7c858 --- /dev/null +++ b/src/mcp/cinema-mcp/main.py @@ -0,0 +1,65 @@ +from typing import Any +import httpx +from mcp.server.fastmcp import FastMCP + +# Initialize FastMCP server +mcp = FastMCP("cinema-mcp", description="Cinema Management") + +@mcp.tool() +async def get_alerts(state: str) -> str: + """Get weather alerts for a US state. + + Args: + state: Two-letter US state code (e.g. CA, NY) + """ + url = f"{NWS_API_BASE}/alerts/active/area/{state}" + data = await make_nws_request(url) + + if not data or "features" not in data: + return "Unable to fetch alerts or no alerts found." + + if not data["features"]: + return "No active alerts for this state." + + alerts = [format_alert(feature) for feature in data["features"]] + return "\n---\n".join(alerts) + +@mcp.tool() +async def get_forecast(latitude: float, longitude: float) -> str: + """Get weather forecast for a location. + + Args: + latitude: Latitude of the location + longitude: Longitude of the location + """ + # First get the forecast grid endpoint + points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" + points_data = await make_nws_request(points_url) + + if not points_data: + return "Unable to fetch forecast data for this location." + + # Get the forecast URL from the points response + forecast_url = points_data["properties"]["forecast"] + forecast_data = await make_nws_request(forecast_url) + + if not forecast_data: + return "Unable to fetch detailed forecast." + + # Format the periods into a readable forecast + periods = forecast_data["properties"]["periods"] + forecasts = [] + for period in periods[:5]: # Only show next 5 periods + forecast = f""" +{period['name']}: +Temperature: {period['temperature']}°{period['temperatureUnit']} +Wind: {period['windSpeed']} {period['windDirection']} +Forecast: {period['detailedForecast']} +""" + forecasts.append(forecast) + + return "\n---\n".join(forecasts) + +if __name__ == "__main__": + # Initialize and run the server + mcp.run(transport='stdio') \ No newline at end of file diff --git a/src/mcp/cinema-mcp/pyproject.toml b/src/mcp/cinema-mcp/pyproject.toml new file mode 100644 index 0000000..6802c1c --- /dev/null +++ b/src/mcp/cinema-mcp/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "cinema-mcp" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.14.1", +] diff --git a/src/mcp/cinema-mcp/uv.lock b/src/mcp/cinema-mcp/uv.lock new file mode 100644 index 0000000..8a46ce2 --- /dev/null +++ b/src/mcp/cinema-mcp/uv.lock @@ -0,0 +1,525 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cinema-mcp" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "mcp", extra = ["cli"] }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.14.1" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e9/242096400d702924b49f8d202c6ded7efb8841cacba826b5d2e6183aef7b/mcp-1.14.1.tar.gz", hash = "sha256:31c4406182ba15e8f30a513042719c3f0a38c615e76188ee5a736aaa89e20134", size = 454944, upload-time = "2025-09-18T13:37:19.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/11/d334fbb7c2aeddd2e762b86d7a619acffae012643a5738e698f975a2a9e2/mcp-1.14.1-py3-none-any.whl", hash = "sha256:3b7a479e8e5cbf5361bdc1da8bc6d500d795dc3aff44b44077a363a7f7e945a4", size = 163809, upload-time = "2025-09-18T13:37:18.165Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +] From 76a236b89e64024829c00a611b45b68c894d82f4 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:16:53 +0100 Subject: [PATCH 05/25] Cinema tooling basis --- src/mcp/cinema-mcp/README.md | 84 +++++ src/mcp/cinema-mcp/__init__.py | 7 + src/mcp/cinema-mcp/cinema_service.py | 237 +++++++++++++ src/mcp/cinema-mcp/config.py | 191 +++++++++++ src/mcp/cinema-mcp/main.py | 145 ++++++++ src/mcp/cinema-mcp/pyproject.toml | 9 + src/mcp/cinema-mcp/utils.py | 205 +++++++++++ src/mcp/cinema-mcp/uv.lock | 486 +++++++++++++++++++++++++++ 8 files changed, 1364 insertions(+) create mode 100644 src/mcp/cinema-mcp/README.md create mode 100644 src/mcp/cinema-mcp/__init__.py create mode 100644 src/mcp/cinema-mcp/cinema_service.py create mode 100644 src/mcp/cinema-mcp/config.py create mode 100644 src/mcp/cinema-mcp/main.py create mode 100644 src/mcp/cinema-mcp/pyproject.toml create mode 100644 src/mcp/cinema-mcp/utils.py create mode 100644 src/mcp/cinema-mcp/uv.lock diff --git a/src/mcp/cinema-mcp/README.md b/src/mcp/cinema-mcp/README.md new file mode 100644 index 0000000..e72b63a --- /dev/null +++ b/src/mcp/cinema-mcp/README.md @@ -0,0 +1,84 @@ +# Cinema MCP Server + +A Model Context Protocol (MCP) server for managing movie theater showings and reservations. + +## Features + +- **Current Movies**: View all currently playing movies with showtimes +- **Movie Details**: Get detailed information about specific movie showings +- **Search Movies**: Filter movies by genre, date, theater room, or seat availability +- **Daily Schedule**: View all movies for a specific date + +## Quick Start + +1. Install dependencies: + ```bash + uv pip install -r requirements.txt + ``` + +2. Run the MCP server: + ```bash + uv run mcp dev main.py + ``` + +3. The server will start on port 8009 + +## Available Tools + +### `get_current_movies()` +Returns all currently playing movies with complete details including showtimes, seat availability, and pricing. + +### `get_movie_details(movie_id: str)` +Get detailed information about a specific movie showing including cast, director, and theater details. + +### `search_movies(genre, date, room, available_seats_min, limit)` +Search and filter movies by various criteria: +- **genre**: action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family +- **date**: YYYY-MM-DD format +- **room**: theater_a, theater_b, theater_c, imax +- **available_seats_min**: minimum seats required +- **limit**: maximum results (default: 20) + +### `get_movies_by_date(date: str)` +Get all movie showings for a specific date, sorted by showtime. + +## Available Resources + +- `movie://{movie_id}` - Formatted movie details +- `cinema://current-movies` - Current movies overview + +## Mock Data + +The server uses `MOCK_MOVIE_PRESENTATIONS` from `config.py` with sample movies including: +- Galactic Adventures (Sci-Fi) +- The Midnight Mystery (Thriller) +- Laugh Out Loud (Comedy) +- Dragon's Heart (Animation) +- City of Shadows (Drama) +- Ocean's Edge (Documentary/IMAX) +- Love in Paris (Romance) +- Nightmare Manor (Horror) + +## Configuration + +Edit `config.py` to modify: +- Theater room configurations +- Movie genres and ratings +- Mock movie data +- Pricing and capacity settings + +## Usage Examples + +```python +# Get all current movies +movies = get_current_movies() + +# Search for action movies with at least 50 seats available +action_movies = search_movies(genre="action", available_seats_min=50) + +# Get movies playing tomorrow +tomorrow_movies = get_movies_by_date("2025-09-26") + +# Get details for a specific movie +movie_details = get_movie_details("movie_001") +``` \ No newline at end of file diff --git a/src/mcp/cinema-mcp/__init__.py b/src/mcp/cinema-mcp/__init__.py new file mode 100644 index 0000000..fc760c0 --- /dev/null +++ b/src/mcp/cinema-mcp/__init__.py @@ -0,0 +1,7 @@ +""" +Cinema MCP Server Package +""" + +__version__ = "1.0.0" +__author__ = "Cinema MCP Team" +__description__ = "Movie theater management and booking MCP server" \ No newline at end of file diff --git a/src/mcp/cinema-mcp/cinema_service.py b/src/mcp/cinema-mcp/cinema_service.py new file mode 100644 index 0000000..43d0123 --- /dev/null +++ b/src/mcp/cinema-mcp/cinema_service.py @@ -0,0 +1,237 @@ +""" +Cinema service with MCP tools and API logic. +""" + +from typing import Dict, Any, List, Optional +from dataclasses import asdict +from datetime import datetime, date, time + +from config import ( + MOCK_MOVIE_PRESENTATIONS, CINEMA_ROOMS, MOVIE_GENRES, + DEFAULT_SEARCH_LIMIT, POPULAR_MOVIES +) +from models import MovieShowing, ContactInfo, MovieReservation +from utils import ( + get_movie_by_id, search_movies_by_criteria, get_current_movies, + get_movies_by_date, format_movie_details, parse_movie_data +) + + +def get_current_movies_data() -> Dict[str, Any]: + """Get all currently playing movies + + Returns: + List of currently playing movies with their details + """ + try: + current_movies = get_current_movies() + + if not current_movies: + return {"error": "No movies currently playing"} + + movies_list = [] + for movie_data in current_movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "id": movie_data["id"], + "title": movie.title, + "description": movie.description, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "seats_remaining": movie.seats_remaining, + "seats_total": movie.seats_available, + "duration_minutes": movie.duration_minutes, + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "price_per_seat": movie_data.get("price_per_seat"), + "director": movie_data.get("director"), + "cast": movie_data.get("cast", []), + "is_sold_out": movie.is_sold_out, + "occupancy_percentage": round(movie.occupancy_percentage, 1) + }) + + return { + "cinema_name": "MovieMagic Cinema", + "total_movies": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to get current movies: {str(e)}"} + + +def get_movie_details_data(movie_id: str) -> Dict[str, Any]: + """Get detailed information about a specific movie + + Args: + movie_id: Unique ID of the movie showing + + Returns: + MovieShowing object as dictionary or error dict + """ + try: + movie_data = get_movie_by_id(movie_id) + if not movie_data: + return {"error": f"Movie with ID {movie_id} not found"} + + movie = parse_movie_data(movie_data) + room_info = CINEMA_ROOMS.get(movie.room, {}) + + return { + "movie": { + "id": movie_data["id"], + "title": movie.title, + "description": movie.description, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "duration_minutes": movie.duration_minutes, + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "director": movie_data.get("director"), + "cast": movie_data.get("cast", []), + "poster_url": movie_data.get("poster_url") + }, + "showing_details": { + "room": room_info.get("name", movie.room), + "room_type": room_info.get("type", "standard"), + "seats_total": movie.seats_available, + "seats_booked": movie.seats_booked, + "seats_remaining": movie.seats_remaining, + "is_sold_out": movie.is_sold_out, + "occupancy_percentage": round(movie.occupancy_percentage, 1), + "price_per_seat": movie_data.get("price_per_seat") + } + } + + except Exception as e: + return {"error": f"Failed to get movie details: {str(e)}"} + + +def search_movies_data( + genre: Optional[str] = None, + date: Optional[str] = None, + room: Optional[str] = None, + available_seats_min: Optional[int] = None, + limit: int = DEFAULT_SEARCH_LIMIT +) -> Dict[str, Any]: + """Search for movies with filters + + Args: + genre: Movie genre filter (e.g., "action", "comedy", "drama") + date: Date filter in YYYY-MM-DD format + room: Cinema room filter + available_seats_min: Minimum available seats required + limit: Maximum number of results (default: 20, max: 100) + + Returns: + Filtered list of movies or error dict + """ + try: + if limit > 100: + limit = 100 + elif limit < 1: + limit = 1 + + # Convert date string to date object if provided + date_filter = None + if date: + try: + date_filter = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + return {"error": "Invalid date format. Use YYYY-MM-DD"} + + movies = search_movies_by_criteria( + genre=genre, + date=date_filter, + room=room, + available_seats_min=available_seats_min, + limit=limit + ) + + if not movies: + return {"error": "No movies found matching the criteria"} + + movies_list = [] + for movie_data in movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "id": movie_data["id"], + "title": movie.title, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "seats_remaining": movie.seats_remaining, + "price_per_seat": movie_data.get("price_per_seat"), + "is_sold_out": movie.is_sold_out + }) + + # Build filter summary + filters_applied = [] + if genre: + filters_applied.append(f"Genre: {MOVIE_GENRES.get(genre, genre)}") + if date: + filters_applied.append(f"Date: {date}") + if room: + filters_applied.append(f"Room: {CINEMA_ROOMS.get(room, {}).get('name', room)}") + if available_seats_min: + filters_applied.append(f"Min available seats: {available_seats_min}") + + return { + "search_criteria": filters_applied if filters_applied else ["No filters applied"], + "total_found": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to search movies: {str(e)}"} + + +def get_movies_by_date_data(date: str) -> Dict[str, Any]: + """Get all movies playing on a specific date + + Args: + date: Date in YYYY-MM-DD format + + Returns: + List of movies for the specified date + """ + try: + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + return {"error": "Invalid date format. Use YYYY-MM-DD"} + + movies = get_movies_by_date(target_date) + + if not movies: + return {"error": f"No movies scheduled for {date}"} + + movies_list = [] + for movie_data in movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "id": movie_data["id"], + "title": movie.title, + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "duration_minutes": movie.duration_minutes, + "seats_remaining": movie.seats_remaining, + "price_per_seat": movie_data.get("price_per_seat"), + "is_sold_out": movie.is_sold_out + }) + + # Sort by time + movies_list.sort(key=lambda x: x["time"]) + + return { + "date": date, + "total_showings": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to get movies for date: {str(e)}"} \ No newline at end of file diff --git a/src/mcp/cinema-mcp/config.py b/src/mcp/cinema-mcp/config.py new file mode 100644 index 0000000..30c5546 --- /dev/null +++ b/src/mcp/cinema-mcp/config.py @@ -0,0 +1,191 @@ +""" +Cinema MCP configuration and mock data. +""" + +from datetime import date, time + +# Cinema configuration +CINEMA_NAME = "MovieMagic Cinema" +CINEMA_LOCATION = "Downtown Plaza" + +# Room/Theater configurations +CINEMA_ROOMS = { + "theater_a": {"name": "Theater A", "capacity": 150, "type": "standard"}, + "theater_b": {"name": "Theater B", "capacity": 200, "type": "premium"}, + "theater_c": {"name": "Theater C", "capacity": 100, "type": "vip"}, + "imax": {"name": "IMAX Theater", "capacity": 300, "type": "imax"} +} + +# Movie genres +MOVIE_GENRES = { + "action": "Action", + "comedy": "Comedy", + "drama": "Drama", + "horror": "Horror", + "sci-fi": "Science Fiction", + "romance": "Romance", + "thriller": "Thriller", + "animation": "Animation", + "documentary": "Documentary", + "family": "Family" +} + +# Movie ratings +MOVIE_RATINGS = ["G", "PG", "PG-13", "R", "NC-17"] + +# Booking status codes +BOOKING_STATUS = { + "pending": "Pending Confirmation", + "confirmed": "Confirmed", + "cancelled": "Cancelled", + "completed": "Completed" +} + +# Default values +DEFAULT_SEARCH_LIMIT = 20 +MAX_SEARCH_LIMIT = 100 + +# Mock movie presentations data for demonstration +MOCK_MOVIE_PRESENTATIONS = [ + { + "id": "movie_001", + "title": "Galactic Adventures", + "description": "An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.", + "date": date(2025, 9, 25), + "time": time(14, 30), # 2:30 PM + "room": "theater_a", + "seats_available": 150, + "seats_booked": 45, + "duration_minutes": 142, + "genre": "sci-fi", + "rating": "PG-13", + "price_per_seat": 12.50, + "director": "Sarah Johnson", + "cast": ["Alex Thompson", "Maria Rodriguez", "James Chen"], + "poster_url": "https://example.com/galactic-adventures.jpg" + }, + { + "id": "movie_002", + "title": "The Midnight Mystery", + "description": "A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.", + "date": date(2025, 9, 25), + "time": time(19, 15), # 7:15 PM + "room": "theater_b", + "seats_available": 200, + "seats_booked": 125, + "duration_minutes": 118, + "genre": "thriller", + "rating": "R", + "price_per_seat": 14.00, + "director": "Michael Davis", + "cast": ["Emma Wilson", "Robert Garcia", "Lisa Park"], + "poster_url": "https://example.com/midnight-mystery.jpg" + }, + { + "id": "movie_003", + "title": "Laugh Out Loud", + "description": "A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.", + "date": date(2025, 9, 25), + "time": time(21, 45), # 9:45 PM + "room": "theater_a", + "seats_available": 150, + "seats_booked": 89, + "duration_minutes": 95, + "genre": "comedy", + "rating": "PG-13", + "price_per_seat": 12.50, + "director": "Jennifer Lee", + "cast": ["Tom Martinez", "Sarah Kim", "David Brown"], + "poster_url": "https://example.com/laugh-out-loud.jpg" + }, + { + "id": "movie_004", + "title": "Dragon's Heart", + "description": "An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.", + "date": date(2025, 9, 26), + "time": time(10, 00), # 10:00 AM + "room": "theater_c", + "seats_available": 100, + "seats_booked": 23, + "duration_minutes": 103, + "genre": "animation", + "rating": "G", + "price_per_seat": 10.00, + "director": "Animation Studios Inc.", + "cast": ["Voice Cast: Amy Johnson", "Mark Stevens", "Luna Rodriguez"], + "poster_url": "https://example.com/dragons-heart.jpg" + }, + { + "id": "movie_005", + "title": "City of Shadows", + "description": "A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.", + "date": date(2025, 9, 26), + "time": time(16, 20), # 4:20 PM + "room": "theater_b", + "seats_available": 200, + "seats_booked": 156, + "duration_minutes": 134, + "genre": "drama", + "rating": "R", + "price_per_seat": 14.00, + "director": "Vincent Romano", + "cast": ["Antonio Silva", "Catherine Moore", "Frank Williams"], + "poster_url": "https://example.com/city-of-shadows.jpg" + }, + { + "id": "movie_006", + "title": "Ocean's Edge", + "description": "A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.", + "date": date(2025, 9, 26), + "time": time(20, 00), # 8:00 PM + "room": "imax", + "seats_available": 300, + "seats_booked": 78, + "duration_minutes": 87, + "genre": "documentary", + "rating": "G", + "price_per_seat": 18.00, + "director": "Ocean Explorer Films", + "cast": ["Narrator: David Attenborough"], + "poster_url": "https://example.com/oceans-edge.jpg" + }, + { + "id": "movie_007", + "title": "Love in Paris", + "description": "A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.", + "date": date(2025, 9, 27), + "time": time(15, 45), # 3:45 PM + "room": "theater_c", + "seats_available": 100, + "seats_booked": 67, + "duration_minutes": 108, + "genre": "romance", + "rating": "PG", + "price_per_seat": 11.50, + "director": "Claire Dubois", + "cast": ["Sophie Martin", "Jean-Luc Moreau", "Isabella Jones"], + "poster_url": "https://example.com/love-in-paris.jpg" + }, + { + "id": "movie_008", + "title": "Nightmare Manor", + "description": "A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.", + "date": date(2025, 9, 27), + "time": time(22, 30), # 10:30 PM + "room": "theater_a", + "seats_available": 150, + "seats_booked": 92, + "duration_minutes": 106, + "genre": "horror", + "rating": "R", + "price_per_seat": 13.00, + "director": "Horror Productions", + "cast": ["Scary Actor 1", "Scary Actor 2", "Scary Actor 3"], + "poster_url": "https://example.com/nightmare-manor.jpg" + } +] + +# World famous movies for random selection +POPULAR_MOVIES = [ + "movie_001", "movie_002", "movie_003", "movie_006" +] \ No newline at end of file diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py new file mode 100644 index 0000000..2caa20f --- /dev/null +++ b/src/mcp/cinema-mcp/main.py @@ -0,0 +1,145 @@ +""" +Cinema MCP Server - Discover and book movie showings. + +To run this server: + uv run mcp dev main.py + +Provides information about current movie showings, seat availability, +and reservation capabilities. +""" + +from typing import Dict, Any, Optional +from mcp.server.fastmcp import FastMCP + +from cinema_service import ( + get_current_movies_data, + get_movie_details_data, + search_movies_data, + get_movies_by_date_data +) + +mcp = FastMCP("Cinema", port=8009) + +# Tools +@mcp.tool() +def get_current_movies() -> Dict[str, Any]: + """Get all currently playing movies with their showtimes and availability + + Returns: + List of all movies currently being shown with details including: + - Movie title, description, and basic info + - Showtimes and theater room assignments + - Seat availability and pricing + - Genre, rating, and duration + - Cast and director information + """ + return get_current_movies_data() + +@mcp.tool() +def get_movie_details(movie_id: str) -> Dict[str, Any]: + """Get detailed information about a specific movie showing + + Args: + movie_id: Unique ID of the movie showing (e.g., "movie_001") + + Returns: + Detailed movie information including plot, cast, theater details, and availability + """ + return get_movie_details_data(movie_id) + +@mcp.tool() +def search_movies( + genre: Optional[str] = None, + date: Optional[str] = None, + room: Optional[str] = None, + available_seats_min: Optional[int] = None, + limit: int = 20 +) -> Dict[str, Any]: + """Search for movies with optional filters + + Args: + genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family) + date: Filter by date in YYYY-MM-DD format (e.g., "2025-09-25") + room: Filter by cinema room (theater_a, theater_b, theater_c, imax) + available_seats_min: Minimum number of available seats required + limit: Maximum number of results to return (default: 20, max: 100) + + Returns: + Filtered list of movies matching the search criteria + """ + return search_movies_data(genre, date, room, available_seats_min, limit) + +@mcp.tool() +def get_movies_by_date(date: str) -> Dict[str, Any]: + """Get all movies playing on a specific date + + Args: + date: Date in YYYY-MM-DD format (e.g., "2025-09-25") + + Returns: + List of all movie showings scheduled for the specified date, sorted by time + """ + return get_movies_by_date_data(date) + +# Resources +@mcp.resource("movie://{movie_id}") +def get_movie_resource(movie_id: str) -> str: + """Get movie details as a formatted resource""" + result = get_movie_details_data(movie_id) + + if "error" in result: + return f"Error: {result['error']}" + + movie = result["movie"] + showing = result["showing_details"] + + return f""" +🎬 {movie['title']} ({movie['rating']}) + +📅 Showing: {movie['date']} at {movie['time']} +🏛️ Theater: {showing['room']} ({showing['room_type']}) +🎭 Genre: {movie['genre']} | ⏱️ Duration: {movie['duration_minutes']} minutes +🎬 Director: {movie['director']} +⭐ Cast: {', '.join(movie['cast'])} + +💺 Seating: +- Total Seats: {showing['seats_total']} +- Available: {showing['seats_remaining']} +- Booked: {showing['seats_booked']} +- Occupancy: {showing['occupancy_percentage']}% +- Price: ${showing['price_per_seat']:.2f} per seat + +📝 Description: +{movie['description']} + +{"🚫 SOLD OUT" if showing['is_sold_out'] else "✅ Tickets Available"} +""" + +@mcp.resource("cinema://current-movies") +def get_current_movies_resource() -> str: + """Get current movies as a formatted resource""" + result = get_current_movies_data() + + if "error" in result: + return f"Error: {result['error']}" + + output = f"🎬 {result['cinema_name']} - Current Movies\n" + output += f"📊 Total Movies Playing: {result['total_movies']}\n\n" + + for i, movie in enumerate(result['movies'], 1): + output += f"{i}. {movie['title']} ({movie['rating']})\n" + output += f" 📅 {movie['date']} at {movie['time']} | 🏛️ {movie['room']}\n" + output += f" 🎭 {movie['genre']} | ⏱️ {movie['duration_minutes']}min | 💰 ${movie['price_per_seat']:.2f}\n" + output += f" 💺 {movie['seats_remaining']}/{movie['seats_total']} seats available" + + if movie['is_sold_out']: + output += " 🚫 SOLD OUT" + else: + output += f" ({movie['occupancy_percentage']}% full)" + + output += "\n\n" + + return output + +if __name__ == "__main__": + mcp.run() \ No newline at end of file diff --git a/src/mcp/cinema-mcp/pyproject.toml b/src/mcp/cinema-mcp/pyproject.toml new file mode 100644 index 0000000..d62e814 --- /dev/null +++ b/src/mcp/cinema-mcp/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "cinema-mcp" +version = "1.0.0" +description = "MCP server for discovering and booking movie showings" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mcp[cli]>=1.13.1", +] \ No newline at end of file diff --git a/src/mcp/cinema-mcp/utils.py b/src/mcp/cinema-mcp/utils.py new file mode 100644 index 0000000..fc8a16e --- /dev/null +++ b/src/mcp/cinema-mcp/utils.py @@ -0,0 +1,205 @@ +""" +Utility functions for cinema operations. +""" + +from typing import Dict, Any, Optional, List +from datetime import datetime, date, time + +from config import MOCK_MOVIE_PRESENTATIONS, CINEMA_ROOMS, MOVIE_GENRES +from models import MovieShowing + + +def get_movie_by_id(movie_id: str) -> Optional[Dict[str, Any]]: + """Get movie details by ID from mock data""" + for movie in MOCK_MOVIE_PRESENTATIONS: + if movie["id"] == movie_id: + return movie + return None + + +def get_current_movies() -> List[Dict[str, Any]]: + """Get all currently playing movies from mock data""" + today = date.today() + current_movies = [] + + for movie in MOCK_MOVIE_PRESENTATIONS: + # For demo purposes, show movies from today and next few days + if movie["date"] >= today: + current_movies.append(movie) + + return current_movies + + +def get_movies_by_date(target_date: date) -> List[Dict[str, Any]]: + """Get all movies playing on a specific date""" + movies_on_date = [] + + for movie in MOCK_MOVIE_PRESENTATIONS: + if movie["date"] == target_date: + movies_on_date.append(movie) + + return movies_on_date + + +def search_movies_by_criteria( + genre: Optional[str] = None, + date: Optional[date] = None, + room: Optional[str] = None, + available_seats_min: Optional[int] = None, + limit: int = 20 +) -> List[Dict[str, Any]]: + """Search movies based on various criteria""" + filtered_movies = [] + + for movie in MOCK_MOVIE_PRESENTATIONS: + # Filter by genre + if genre and movie.get("genre") != genre: + continue + + # Filter by date + if date and movie.get("date") != date: + continue + + # Filter by room + if room and movie.get("room") != room: + continue + + # Filter by minimum available seats + if available_seats_min: + seats_available = movie.get("seats_available", 0) + seats_booked = movie.get("seats_booked", 0) + seats_remaining = seats_available - seats_booked + if seats_remaining < available_seats_min: + continue + + filtered_movies.append(movie) + + # Apply limit + if len(filtered_movies) >= limit: + break + + return filtered_movies + + +def parse_movie_data(movie_data: Dict[str, Any]) -> MovieShowing: + """Parse movie data from mock data into MovieShowing object""" + return MovieShowing( + title=movie_data.get("title", ""), + description=movie_data.get("description", ""), + date=movie_data.get("date"), + time=movie_data.get("time"), + room=movie_data.get("room", ""), + seats_available=movie_data.get("seats_available", 0), + seats_booked=movie_data.get("seats_booked", 0), + duration_minutes=movie_data.get("duration_minutes"), + genre=movie_data.get("genre"), + rating=movie_data.get("rating") + ) + + +def format_movie_details(movie: MovieShowing, movie_data: Dict[str, Any]) -> str: + """Format movie details as a readable string""" + room_info = CINEMA_ROOMS.get(movie.room, {}) + genre_name = MOVIE_GENRES.get(movie.genre, movie.genre) if movie.genre else "Unknown" + + details = f"🎬 {movie.title}\n" + details += f"📅 Date: {movie.date.strftime('%A, %B %d, %Y')}\n" + details += f"🕒 Time: {movie.time.strftime('%I:%M %p')}\n" + details += f"🏛️ Theater: {room_info.get('name', movie.room)}\n" + details += f"🎭 Genre: {genre_name}\n" + + if movie.rating: + details += f"🏷️ Rating: {movie.rating}\n" + + if movie.duration_minutes: + hours = movie.duration_minutes // 60 + minutes = movie.duration_minutes % 60 + if hours > 0: + details += f"⏱️ Duration: {hours}h {minutes}m\n" + else: + details += f"⏱️ Duration: {minutes}m\n" + + details += f"💺 Seats: {movie.seats_remaining} available / {movie.seats_available} total\n" + + if movie.is_sold_out: + details += f"🚫 Status: SOLD OUT\n" + else: + details += f"📊 Occupancy: {movie.occupancy_percentage:.1f}%\n" + + if movie_data.get("price_per_seat"): + details += f"💰 Price: ${movie_data['price_per_seat']:.2f} per seat\n" + + if movie_data.get("director"): + details += f"🎬 Director: {movie_data['director']}\n" + + if movie_data.get("cast"): + cast_list = movie_data["cast"][:3] # Show first 3 cast members + details += f"⭐ Cast: {', '.join(cast_list)}\n" + + if movie.description: + details += f"\n📝 Description: {movie.description}\n" + + return details + + +def format_showtime(movie_time: time) -> str: + """Format time in a user-friendly way""" + return movie_time.strftime("%I:%M %p").lstrip("0") + + +def get_room_display_name(room_key: str) -> str: + """Get display name for cinema room""" + room_info = CINEMA_ROOMS.get(room_key, {}) + return room_info.get("name", room_key.replace("_", " ").title()) + + +def calculate_total_price(num_seats: int, price_per_seat: float) -> float: + """Calculate total price for reservation""" + return num_seats * price_per_seat + + +def validate_movie_date(movie_date: date) -> bool: + """Validate that movie date is not in the past""" + return movie_date >= date.today() + + +def validate_seat_availability(movie_data: Dict[str, Any], requested_seats: int) -> bool: + """Check if enough seats are available for booking""" + seats_available = movie_data.get("seats_available", 0) + seats_booked = movie_data.get("seats_booked", 0) + seats_remaining = seats_available - seats_booked + return seats_remaining >= requested_seats + + +def get_popular_movies() -> List[Dict[str, Any]]: + """Get popular movies from mock data""" + from config import POPULAR_MOVIES + popular = [] + + for movie_id in POPULAR_MOVIES: + movie_data = get_movie_by_id(movie_id) + if movie_data: + popular.append(movie_data) + + return popular + + +def format_movie_summary(movie_data: Dict[str, Any]) -> str: + """Create a short summary of a movie showing""" + movie_time = movie_data.get("time") + room_name = get_room_display_name(movie_data.get("room", "")) + genre_name = MOVIE_GENRES.get(movie_data.get("genre"), movie_data.get("genre", "")) + + seats_available = movie_data.get("seats_available", 0) + seats_booked = movie_data.get("seats_booked", 0) + seats_remaining = seats_available - seats_booked + + time_str = format_showtime(movie_time) if movie_time else "TBA" + + summary = f"{movie_data.get('title', 'Untitled')} - {time_str} in {room_name}" + summary += f" | {genre_name} | {seats_remaining} seats left" + + if movie_data.get("rating"): + summary += f" | Rated {movie_data['rating']}" + + return summary \ No newline at end of file diff --git a/src/mcp/cinema-mcp/uv.lock b/src/mcp/cinema-mcp/uv.lock new file mode 100644 index 0000000..7ee26ae --- /dev/null +++ b/src/mcp/cinema-mcp/uv.lock @@ -0,0 +1,486 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cinema-mcp" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, +] + +[package.metadata] +requires-dist = [{ name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e9/242096400d702924b49f8d202c6ded7efb8841cacba826b5d2e6183aef7b/mcp-1.14.1.tar.gz", hash = "sha256:31c4406182ba15e8f30a513042719c3f0a38c615e76188ee5a736aaa89e20134", size = 454944, upload-time = "2025-09-18T13:37:19.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/11/d334fbb7c2aeddd2e762b86d7a619acffae012643a5738e698f975a2a9e2/mcp-1.14.1-py3-none-any.whl", hash = "sha256:3b7a479e8e5cbf5361bdc1da8bc6d500d795dc3aff44b44077a363a7f7e945a4", size = 163809, upload-time = "2025-09-18T13:37:18.165Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +] From a979bfe82a4f2018efa53be94421abf07bfa40c5 Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 12:23:46 +0100 Subject: [PATCH 06/25] initial python version --- src/mcp/movie-reviews-mcp/.gitignore | 1 - src/mcp/movie-reviews-mcp/__init__.py | 51 + .../movie-reviews-mcp/attractions_service.py | 306 +++++ src/mcp/movie-reviews-mcp/config.py | 240 ++++ src/mcp/movie-reviews-mcp/main.py | 192 +++ src/mcp/movie-reviews-mcp/models.py | 82 ++ src/mcp/movie-reviews-mcp/package-lock.json | 1103 ----------------- src/mcp/movie-reviews-mcp/package.json | 22 - src/mcp/movie-reviews-mcp/pyproject.toml | 10 + src/mcp/movie-reviews-mcp/readme.md | 169 ++- src/mcp/movie-reviews-mcp/src/index.ts | 231 ---- src/mcp/movie-reviews-mcp/tsconfig.json | 15 - src/mcp/movie-reviews-mcp/utils.py | 214 ++++ src/mcp/movie-reviews-mcp/uv.lock | 545 ++++++++ 14 files changed, 1797 insertions(+), 1384 deletions(-) delete mode 100644 src/mcp/movie-reviews-mcp/.gitignore create mode 100644 src/mcp/movie-reviews-mcp/__init__.py create mode 100644 src/mcp/movie-reviews-mcp/attractions_service.py create mode 100644 src/mcp/movie-reviews-mcp/config.py create mode 100644 src/mcp/movie-reviews-mcp/main.py create mode 100644 src/mcp/movie-reviews-mcp/models.py delete mode 100644 src/mcp/movie-reviews-mcp/package-lock.json delete mode 100644 src/mcp/movie-reviews-mcp/package.json create mode 100644 src/mcp/movie-reviews-mcp/pyproject.toml delete mode 100644 src/mcp/movie-reviews-mcp/src/index.ts delete mode 100644 src/mcp/movie-reviews-mcp/tsconfig.json create mode 100644 src/mcp/movie-reviews-mcp/utils.py create mode 100644 src/mcp/movie-reviews-mcp/uv.lock diff --git a/src/mcp/movie-reviews-mcp/.gitignore b/src/mcp/movie-reviews-mcp/.gitignore deleted file mode 100644 index b512c09..0000000 --- a/src/mcp/movie-reviews-mcp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/__init__.py b/src/mcp/movie-reviews-mcp/__init__.py new file mode 100644 index 0000000..0ed8e76 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/__init__.py @@ -0,0 +1,51 @@ +""" +Tourist Attractions MCP Package - Discover and book attractions worldwide. +""" + +from attractions_service import ( + get_attraction_details_data, + search_attractions_data, + get_random_attraction_data, + get_world_wonders_data, + book_attraction_data, + get_attraction_categories_data +) +from models import ( + Attraction, AttractionDetails, BookingRequest, BookingResponse, + AttractionsList, SearchFilters, Location, Coordinates +) +from utils import ( + get_attraction_by_id, search_attractions, parse_attraction_data, + format_attraction_name, get_category_display_name, generate_booking_id, + validate_visit_date, validate_email, format_attraction_details +) + +__version__ = "1.0.0" +__all__ = [ + # Service functions + "get_attraction_details_data", + "search_attractions_data", + "get_random_attraction_data", + "get_world_wonders_data", + "book_attraction_data", + "get_attraction_categories_data", + # Models + "Attraction", + "AttractionDetails", + "BookingRequest", + "BookingResponse", + "AttractionsList", + "SearchFilters", + "Location", + "Coordinates", + # Utilities + "get_attraction_by_id", + "search_attractions", + "parse_attraction_data", + "format_attraction_name", + "get_category_display_name", + "generate_booking_id", + "validate_visit_date", + "validate_email", + "format_attraction_details" +] diff --git a/src/mcp/movie-reviews-mcp/attractions_service.py b/src/mcp/movie-reviews-mcp/attractions_service.py new file mode 100644 index 0000000..57bcdab --- /dev/null +++ b/src/mcp/movie-reviews-mcp/attractions_service.py @@ -0,0 +1,306 @@ +""" +Attractions service with MCP tools and API logic. +""" + +from typing import Dict, Any, List +from dataclasses import asdict +from datetime import datetime + +from config import DEFAULT_SEARCH_LIMIT, ATTRACTION_CATEGORIES +from models import ( + AttractionDetails, BookingRequest, BookingResponse, + AttractionsList, SearchFilters +) +from utils import ( + get_attraction_by_id, search_attractions, get_random_famous_attraction, + get_random_india_attraction, get_wonders_of_world, parse_attraction_data, + format_attraction_name, get_category_display_name, generate_booking_id, + generate_confirmation_code, validate_visit_date, validate_email, + calculate_estimated_cost, format_attraction_details +) + + +def get_attraction_details_data(attraction_id: int) -> Dict[str, Any]: + """Get detailed information about a specific attraction + + Args: + attraction_id: Unique ID of the attraction + + Returns: + AttractionDetails object as dictionary or error dict + """ + try: + data = get_attraction_by_id(attraction_id) + if not data: + return {"error": f"Attraction with ID {attraction_id} not found"} + + attraction = parse_attraction_data(data) + + attraction_details = AttractionDetails( + attraction=attraction, + reviews_count=data.get("reviews_count"), + facilities=data.get("facilities", []), + best_time_to_visit=data.get("best_time_to_visit"), + duration=data.get("duration") + ) + + return asdict(attraction_details) + + except Exception as e: + return {"error": f"Failed to get attraction details: {str(e)}"} + + +def search_attractions_data( + location: str = None, + category: str = None, + limit: int = DEFAULT_SEARCH_LIMIT +) -> Dict[str, Any]: + """Search for attractions with filters + + Args: + location: Location to search in (e.g., "Paris", "India", "Italy") + category: Category of attractions (e.g., "historical", "natural", "cultural") + limit: Maximum number of results (default: 20, max: 100) + + Returns: + AttractionsList object as dictionary or error dict + """ + try: + if limit > 100: + limit = 100 + elif limit < 1: + limit = 1 + + data = search_attractions(location, category, limit) + if not data: + return {"error": "No attractions found matching the criteria"} + + attractions = [] + if "attractions" in data: + for item in data["attractions"]: + attraction = parse_attraction_data(item) + attractions.append(attraction) + + attractions_list = AttractionsList( + category=get_category_display_name(category) if category else "All Categories", + location=location or "Worldwide", + total_count=data.get("total", len(attractions)), + attractions=attractions + ) + + return asdict(attractions_list) + + except Exception as e: + return {"error": f"Failed to search attractions: {str(e)}"} + + +def get_random_attraction_data(region: str = "famous") -> Dict[str, Any]: + """Get a random attraction + + Args: + region: Region type - "famous" for world famous attractions, "india" for Indian attractions + + Returns: + Attraction object as dictionary or error dict + """ + try: + if region.lower() == "india": + data = get_random_india_attraction() + else: + data = get_random_famous_attraction() + + if not data: + return {"error": f"No random attraction found for region: {region}"} + + attraction = parse_attraction_data(data) + return asdict(attraction) + + except Exception as e: + return {"error": f"Failed to get random attraction: {str(e)}"} + + +def get_world_wonders_data() -> str: + """Get list of world wonders attractions as formatted string + + Returns: + Formatted string with world wonders attractions + """ + try: + data = get_wonders_of_world() + if not data: + return "Error: No world wonders found" + + attractions = [] + if "attractions" in data: + for item in data["attractions"]: + attraction = parse_attraction_data(item) + attractions.append(attraction) + + result = "🌟 **Wonders of the World**\n\n" + result += f"Total Wonders: {len(attractions)}\n\n" + + for i, attraction in enumerate(attractions, 1): + location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country + + result += f"{i}. 🏛️ **{attraction.name}**\n" + result += f" 📍 {location_str}\n" + result += f" 🏷️ {get_category_display_name(attraction.category)}\n" + + if attraction.rating: + result += f" ⭐ {attraction.rating}/5.0\n" + if attraction.entry_fee: + result += f" 💰 {attraction.entry_fee}\n" + if attraction.description: + result += f" 📝 {attraction.description}\n" + + result += "\n" + + return result + + except Exception as e: + return f"Error: Failed to get world wonders: {str(e)}" + + +def book_attraction_data( + attraction_id: int, + visitor_name: str, + email: str, + visit_date: str, + num_visitors: int = 1, + phone: str = None, + special_requirements: str = None +) -> Dict[str, Any]: + """Book an attraction visit + + Args: + attraction_id: ID of the attraction to book + visitor_name: Name of the primary visitor + email: Email address for booking confirmation + visit_date: Visit date in YYYY-MM-DD format + num_visitors: Number of visitors (default: 1) + phone: Optional phone number + special_requirements: Optional special requirements + + Returns: + BookingResponse object as dictionary or error dict + """ + try: + if not visitor_name.strip(): + return {"error": "Visitor name is required"} + + if not validate_email(email): + return {"error": "Invalid email address"} + + if not validate_visit_date(visit_date): + return {"error": "Visit date must be in the future and in YYYY-MM-DD format"} + + if num_visitors < 1 or num_visitors > 50: + return {"error": "Number of visitors must be between 1 and 50"} + + # Get attraction details for cost calculation + attraction_data = get_attraction_by_id(attraction_id) + if not attraction_data: + return {"error": f"Attraction with ID {attraction_id} not found"} + + attraction = parse_attraction_data(attraction_data) + + # Calculate estimated cost + total_cost = calculate_estimated_cost(num_visitors, attraction.entry_fee) + + # Create booking + booking_id = generate_booking_id() + confirmation_code = generate_confirmation_code() + + booking_response = BookingResponse( + booking_id=booking_id, + attraction_id=attraction_id, + visitor_name=visitor_name, + visit_date=visit_date, + num_visitors=num_visitors, + total_cost=total_cost, + booking_status="confirmed", + confirmation_code=confirmation_code + ) + + return asdict(booking_response) + + except Exception as e: + return {"error": f"Failed to book attraction: {str(e)}"} + + +def get_attraction_categories_data() -> str: + """Get list of available attraction categories as formatted string + + Returns: + Formatted string with category codes and display names + """ + try: + result = "🏛️ **Available Attraction Categories**\n\n" + result += f"Total Categories: {len(ATTRACTION_CATEGORIES)}\n\n" + + for code, display_name in ATTRACTION_CATEGORIES.items(): + result += f"• **{code}**: {display_name}\n" + + result += "\n*Use these category codes when searching for attractions.*" + return result + + except Exception as e: + return f"Error: Failed to get categories: {str(e)}" + + +def format_attraction_resource(attraction_id: int) -> str: + """Get attraction information as a formatted resource""" + data = get_attraction_details_data(attraction_id) + if "error" in data: + return f"Error: {data['error']}" + + attraction_data = data['attraction'] + attraction = parse_attraction_data(attraction_data) + + return format_attraction_details(attraction) + + +def get_booking_summary_prompt(location: str, category: str = None) -> str: + """Generate a prompt for attraction booking summary""" + base = f"Please provide a summary of top attractions in {location}" + if category: + base += f" focusing on {get_category_display_name(category)} attractions" + base += ", including booking recommendations, best times to visit, and practical travel advice." + return base + + +def format_search_results(search_data: Dict[str, Any]) -> str: + """Format search results as a readable string""" + if "error" in search_data: + return f"Error: {search_data['error']}" + + attractions = search_data.get('attractions', []) + if not attractions: + return "No attractions found matching your criteria." + + result = f"🎯 Found {search_data.get('total_count', len(attractions))} attractions" + if search_data.get('location') != "Worldwide": + result += f" in {search_data['location']}" + if search_data.get('category') != "All Categories": + result += f" ({search_data['category']})" + result += ":\n\n" + + for i, attraction_data in enumerate(attractions[:10], 1): # Show first 10 + attraction = parse_attraction_data(attraction_data) + location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country + + result += f"{i}. 🏛️ **{attraction.name}**\n" + result += f" 📍 {location_str}\n" + result += f" 🏷️ {get_category_display_name(attraction.category)}\n" + + if attraction.rating: + result += f" ⭐ {attraction.rating}/5.0\n" + if attraction.entry_fee: + result += f" 💰 {attraction.entry_fee}\n" + + result += "\n" + + if len(attractions) > 10: + result += f"... and {len(attractions) - 10} more attractions\n" + + return result diff --git a/src/mcp/movie-reviews-mcp/config.py b/src/mcp/movie-reviews-mcp/config.py new file mode 100644 index 0000000..7500597 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/config.py @@ -0,0 +1,240 @@ +""" +Tourist attractions MCP configuration mocks data. +""" + +# World Tourist Attractions API configuration +ATTRACTIONS_BASE_URL = "https://www.world-tourist-attractions-api.com" +API_VERSION = "v1" + +# API Endpoints +ENDPOINTS = { + "attraction_by_id": f"/api/{API_VERSION}/attraction", + "random_famous": f"/api/{API_VERSION}/random/famous", + "random_india": f"/api/{API_VERSION}/random/india", + "wonders": f"/api/{API_VERSION}/wonders", + "search": f"/api/{API_VERSION}/search", + "categories": f"/api/{API_VERSION}/categories" +} + +# Attraction categories +ATTRACTION_CATEGORIES = { + "historical": "Historical Sites", + "natural": "Natural Wonders", + "cultural": "Cultural Sites", + "religious": "Religious Sites", + "modern": "Modern Attractions", + "museums": "Museums", + "parks": "Parks & Gardens", + "beaches": "Beaches", + "mountains": "Mountains", + "architecture": "Architecture", + "entertainment": "Entertainment", + "adventure": "Adventure Sports" +} + +# Popular countries/regions +POPULAR_REGIONS = [ + "India", "France", "Italy", "Spain", "Greece", "Egypt", + "Thailand", "Japan", "China", "USA", "UK", "Germany", + "Turkey", "Morocco", "Brazil", "Peru", "Australia" +] + +# Booking status codes +BOOKING_STATUS = { + "pending": "Pending Confirmation", + "confirmed": "Confirmed", + "cancelled": "Cancelled", + "completed": "Completed" +} + +# Default values +DEFAULT_SEARCH_LIMIT = 20 +MAX_SEARCH_LIMIT = 100 +DEFAULT_RATING_MIN = 3.0 + +# Mock attractions data for demonstration +MOCK_ATTRACTIONS = [ + { + "id": 1, + "name": "Eiffel Tower", + "description": "Iconic iron lattice tower located on the Champ de Mars in Paris, France", + "category": "architecture", + "location": {"city": "Paris", "country": "France", "region": "Île-de-France"}, + "rating": 4.6, + "image_url": "https://example.com/eiffel-tower.jpg", + "website": "https://www.toureiffel.paris", + "opening_hours": "9:30 AM - 11:45 PM", + "entry_fee": "€29.40 - €73.30" + }, + { + "id": 2, + "name": "Taj Mahal", + "description": "Ivory-white marble mausoleum on the right bank of the river Yamuna in Agra", + "category": "historical", + "location": {"city": "Agra", "country": "India", "region": "Uttar Pradesh"}, + "rating": 4.8, + "image_url": "https://example.com/taj-mahal.jpg", + "website": "https://www.tajmahal.gov.in", + "opening_hours": "6:00 AM - 7:00 PM", + "entry_fee": "₹1100 (foreigners), ₹50 (Indians)" + }, + { + "id": 3, + "name": "Colosseum", + "description": "Ancient Roman amphitheater in the center of Rome, Italy", + "category": "historical", + "location": {"city": "Rome", "country": "Italy", "region": "Lazio"}, + "rating": 4.5, + "image_url": "https://example.com/colosseum.jpg", + "website": "https://www.coopculture.it", + "opening_hours": "8:30 AM - 7:15 PM", + "entry_fee": "€16 - €22" + }, + { + "id": 4, + "name": "Machu Picchu", + "description": "Ancient Incan city set high in the Andes Mountains of Peru", + "category": "historical", + "location": {"city": "Cusco", "country": "Peru", "region": "Cusco"}, + "rating": 4.9, + "image_url": "https://example.com/machu-picchu.jpg", + "website": "https://www.machupicchu.gob.pe", + "opening_hours": "6:00 AM - 5:30 PM", + "entry_fee": "$47 - $62" + }, + { + "id": 5, + "name": "Louvre Museum", + "description": "World's largest art museum and historic monument in Paris", + "category": "museums", + "location": {"city": "Paris", "country": "France", "region": "Île-de-France"}, + "rating": 4.4, + "image_url": "https://example.com/louvre.jpg", + "website": "https://www.louvre.fr", + "opening_hours": "9:00 AM - 6:00 PM", + "entry_fee": "€17" + }, + { + "id": 6, + "name": "Great Wall of China", + "description": "Ancient fortification built across northern China", + "category": "historical", + "location": {"city": "Beijing", "country": "China", "region": "Beijing"}, + "rating": 4.7, + "image_url": "https://example.com/great-wall.jpg", + "website": "https://www.mutianyu.com", + "opening_hours": "7:30 AM - 5:30 PM", + "entry_fee": "¥45 - ¥65" + }, + { + "id": 7, + "name": "Santorini", + "description": "Beautiful Greek island with white buildings and blue domes", + "category": "natural", + "location": {"city": "Santorini", "country": "Greece", "region": "Cyclades"}, + "rating": 4.6, + "image_url": "https://example.com/santorini.jpg", + "website": "https://www.santorini.com", + "opening_hours": "24/7", + "entry_fee": "Free" + }, + { + "id": 8, + "name": "Angkor Wat", + "description": "Largest religious monument in the world, originally a Hindu temple", + "category": "religious", + "location": {"city": "Siem Reap", "country": "Cambodia", "region": "Siem Reap"}, + "rating": 4.8, + "image_url": "https://example.com/angkor-wat.jpg", + "website": "https://www.angkorwat.com", + "opening_hours": "5:00 AM - 6:00 PM", + "entry_fee": "$37 (1 day), $62 (3 days)" + }, + { + "id": 9, + "name": "Central Park", + "description": "Large public park in Manhattan, New York City", + "category": "parks", + "location": {"city": "New York", "country": "USA", "region": "New York"}, + "rating": 4.3, + "image_url": "https://example.com/central-park.jpg", + "website": "https://www.centralparknyc.org", + "opening_hours": "6:00 AM - 1:00 AM", + "entry_fee": "Free" + }, + { + "id": 10, + "name": "Petra", + "description": "Archaeological city famous for rock-cut architecture and water conduit system", + "category": "historical", + "location": {"city": "Ma'an", "country": "Jordan", "region": "Ma'an"}, + "rating": 4.7, + "image_url": "https://example.com/petra.jpg", + "website": "https://www.visitpetra.jo", + "opening_hours": "6:00 AM - 6:00 PM", + "entry_fee": "70 JOD (1 day), 55 JOD (2 days)" + }, + { + "id": 11, + "name": "Statue of Liberty", + "description": "Neoclassical sculpture on Liberty Island in New York Harbor", + "category": "modern", + "location": {"city": "New York", "country": "USA", "region": "New York"}, + "rating": 4.4, + "image_url": "https://example.com/statue-liberty.jpg", + "website": "https://www.nps.gov/stli", + "opening_hours": "8:30 AM - 4:00 PM", + "entry_fee": "$23.80 - $24.30" + }, + { + "id": 12, + "name": "Sagrada Familia", + "description": "Unfinished Roman Catholic minor basilica in Barcelona, Spain", + "category": "religious", + "location": {"city": "Barcelona", "country": "Spain", "region": "Catalonia"}, + "rating": 4.6, + "image_url": "https://example.com/sagrada-familia.jpg", + "website": "https://sagradafamilia.org", + "opening_hours": "9:00 AM - 8:00 PM", + "entry_fee": "€26 - €40" + }, + { + "id": 13, + "name": "Kinkaku-ji", + "description": "Golden Pavilion, a Zen temple in Kyoto, Japan", + "category": "religious", + "location": {"city": "Kyoto", "country": "Japan", "region": "Kansai"}, + "rating": 4.5, + "image_url": "https://example.com/kinkaku-ji.jpg", + "website": "https://www.shokoku-ji.jp", + "opening_hours": "9:00 AM - 5:00 PM", + "entry_fee": "¥500" + }, + { + "id": 14, + "name": "Sydney Opera House", + "description": "Multi-venue performing arts center in Sydney, Australia", + "category": "modern", + "location": {"city": "Sydney", "country": "Australia", "region": "New South Wales"}, + "rating": 4.4, + "image_url": "https://example.com/sydney-opera.jpg", + "website": "https://www.sydneyoperahouse.com", + "opening_hours": "9:00 AM - 8:30 PM", + "entry_fee": "$43 - $175" + }, + { + "id": 15, + "name": "Christ the Redeemer", + "description": "Art Deco statue of Jesus Christ in Rio de Janeiro, Brazil", + "category": "religious", + "location": {"city": "Rio de Janeiro", "country": "Brazil", "region": "Rio de Janeiro"}, + "rating": 4.5, + "image_url": "https://example.com/christ-redeemer.jpg", + "website": "https://www.cristoredentor.com.br", + "opening_hours": "8:00 AM - 7:00 PM", + "entry_fee": "R$65 - R$98" + } +] + +# World wonders mock data +WORLD_WONDERS = [1, 2, 3, 4, 6, 8, 10, 15] # IDs from MOCK_ATTRACTIONS that are world wonders \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py new file mode 100644 index 0000000..5bd10d3 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/main.py @@ -0,0 +1,192 @@ +""" +Tourist Attractions MCP Server - Discover and book attractions worldwide. + +To run this server: + uv run mcp dev main.py + +Uses World Tourist Attractions API to provide information about tourist attractions +and booking capabilities. +""" + +from typing import Dict, Any, Optional +from mcp.server.fastmcp import FastMCP + +from attractions_service import ( + get_attraction_details_data, + search_attractions_data, + get_random_attraction_data, + get_world_wonders_data, + book_attraction_data, + get_attraction_categories_data, + format_attraction_resource, + get_booking_summary_prompt, + format_search_results +) + +mcp = FastMCP("Attractions", port=8008) + +# tools +@mcp.tool() +def get_attraction_details(attraction_id: int) -> Dict[str, Any]: + """Get detailed information about a specific tourist attraction + + Args: + attraction_id: Unique ID of the attraction + + Returns: + AttractionDetails object as dictionary with attraction info, facilities, and visiting tips + """ + return get_attraction_details_data(attraction_id) + +@mcp.tool() +def search_attractions( + location: Optional[str] = None, + category: Optional[str] = None, + limit: int = 20 +) -> Dict[str, Any]: + """Search for tourist attractions with optional filters + + Args: + location: Location to search in (e.g., "Paris", "India", "Italy") + category: Category filter - "historical", "natural", "cultural", "religious", "modern", "museums", "parks", "beaches", "mountains", "architecture", "entertainment", "adventure" + limit: Maximum number of results (1-100, default: 20) + + Returns: + AttractionsList object as dictionary with matching attractions + """ + return search_attractions_data(location, category, limit) + +@mcp.tool() +def get_random_attraction(region: str = "famous") -> Dict[str, Any]: + """Get a random tourist attraction for inspiration + + Args: + region: Region type - "famous" for world famous attractions, "india" for Indian attractions + + Returns: + Attraction object as dictionary with random attraction details + """ + return get_random_attraction_data(region) + +@mcp.tool() +def book_attraction( + attraction_id: int, + visitor_name: str, + email: str, + visit_date: str, + num_visitors: int = 1, + phone: Optional[str] = None, + special_requirements: Optional[str] = None +) -> Dict[str, Any]: + """Book a visit to a tourist attraction + + Args: + attraction_id: ID of the attraction to book + visitor_name: Name of the primary visitor + email: Email address for booking confirmation + visit_date: Visit date in YYYY-MM-DD format + num_visitors: Number of visitors (1-50, default: 1) + phone: Optional phone number + special_requirements: Optional special requirements or requests + + Returns: + BookingResponse object as dictionary with booking confirmation details + """ + return book_attraction_data( + attraction_id, visitor_name, email, visit_date, + num_visitors, phone, special_requirements + ) + +@mcp.tool() +def search_and_format_attractions( + location: Optional[str] = None, + category: Optional[str] = None, + limit: int = 10 +) -> str: + """Search for attractions and return formatted results for easy reading + + Args: + location: Location to search in (e.g., "Paris", "India", "Italy") + category: Category filter (e.g., "historical", "natural", "cultural") + limit: Maximum number of results (1-20, default: 10) + + Returns: + Formatted string with attraction search results + """ + if limit > 20: + limit = 20 + + search_data = search_attractions_data(location, category, limit) + return format_search_results(search_data) + +# resources +@mcp.resource("attraction://{attraction_id}") +def get_attraction_resource(attraction_id: int) -> str: + """Get attraction information as a formatted resource""" + return format_attraction_resource(attraction_id) + +@mcp.resource("attractions://search/{location}") +def get_attractions_by_location_resource(location: str) -> str: + """Get attractions for a location as a formatted resource""" + search_data = search_attractions_data(location=location, limit=10) + return format_search_results(search_data) + +@mcp.resource("attractions://category/{category}") +def get_attractions_by_category_resource(category: str) -> str: + """Get attractions by category as a formatted resource""" + search_data = search_attractions_data(category=category, limit=10) + return format_search_results(search_data) + +@mcp.resource("attractions://category") +def get_attractions() -> str: + """Get 10 attractions by formatted resource""" + search_data = search_attractions_data(limit=10) + return format_search_results(search_data) + +@mcp.resource("attractions://categories") +def get_attraction_categories_resource() -> str: + """Get list of available attraction categories as a formatted resource""" + return get_attraction_categories_data() + +@mcp.resource("attractions://wonders") +def get_world_wonders_resource() -> str: + """Get list of the Wonders of the World attractions as a formatted resource""" + return get_world_wonders_data() + +# prompts +@mcp.prompt() +def attraction_booking_prompt(location: str, category: Optional[str] = None) -> str: + """Generate a prompt for attraction booking assistance""" + return get_booking_summary_prompt(location, category) + +@mcp.prompt() +def travel_planning_prompt(location: str, days: int = 3) -> str: + """Generate a prompt for travel planning with attractions""" + return f"""Please help plan a {days}-day itinerary for {location}, including: +1. Top must-see attractions and landmarks +2. Best time to visit each attraction +3. Recommended booking strategies and timing +4. Transportation between attractions +5. Estimated costs and budgeting tips +6. Cultural considerations and local customs +7. Alternative attractions if main ones are crowded + +Focus on creating a balanced mix of historical, cultural, and recreational activities suitable for different interests.""" + +@mcp.prompt() +def attraction_comparison_prompt(attraction_ids: str) -> str: + """Generate a prompt for comparing multiple attractions""" + return f"""Please provide a detailed comparison of these attractions (IDs: {attraction_ids}), including: +1. Unique features and highlights of each +2. Best times to visit and crowd levels +3. Entry requirements and booking procedures +4. Approximate visit duration +5. Nearby attractions and activities +6. Accessibility and facilities +7. Value for money assessment +8. Personal recommendations based on different travel styles + +Help decide which attractions to prioritize based on time, budget, and interests.""" + +if __name__ == "__main__": + mcp.run(transport="streamable-http") \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/models.py b/src/mcp/movie-reviews-mcp/models.py new file mode 100644 index 0000000..920cc27 --- /dev/null +++ b/src/mcp/movie-reviews-mcp/models.py @@ -0,0 +1,82 @@ +""" +Tourist attractions data models - Data classes for attractions objects. +""" + +from dataclasses import dataclass +from typing import Optional, List + + +@dataclass +class Coordinates: + lat: float + lon: float + + +@dataclass +class Location: + city: str + country: str + region: Optional[str] = "" + coordinates: Optional[Coordinates] = None + + +@dataclass +class Attraction: + id: int + name: str + description: str + category: str + location: Location + rating: Optional[float] = None + image_url: Optional[str] = None + website: Optional[str] = None + opening_hours: Optional[str] = None + entry_fee: Optional[str] = None + + +@dataclass +class AttractionDetails: + attraction: Attraction + reviews_count: Optional[int] = None + facilities: Optional[List[str]] = None + best_time_to_visit: Optional[str] = None + duration: Optional[str] = None + + +@dataclass +class BookingRequest: + attraction_id: int + visitor_name: str + email: str + visit_date: str + num_visitors: int = 1 + phone: Optional[str] = None + special_requirements: Optional[str] = None + + +@dataclass +class BookingResponse: + booking_id: str + attraction_id: int + visitor_name: str + visit_date: str + num_visitors: int + total_cost: Optional[float] = None + booking_status: str = "confirmed" + confirmation_code: Optional[str] = None + + +@dataclass +class AttractionsList: + category: str + location: Optional[str] = None + total_count: int = 0 + attractions: List[Attraction] = None + + +@dataclass +class SearchFilters: + location: Optional[str] = None + category: Optional[str] = None + rating_min: Optional[float] = None + free_entry: Optional[bool] = None diff --git a/src/mcp/movie-reviews-mcp/package-lock.json b/src/mcp/movie-reviews-mcp/package-lock.json deleted file mode 100644 index 2df4554..0000000 --- a/src/mcp/movie-reviews-mcp/package-lock.json +++ /dev/null @@ -1,1103 +0,0 @@ -{ - "name": "movie-reviews-mcp", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "movie-reviews-mcp", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.1", - "zod": "^3.25.76" - }, - "devDependencies": { - "@types/node": "^24.5.2", - "typescript": "^5.9.2" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", - "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.6", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.12.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "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/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "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/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "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/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/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/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/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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/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/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "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/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - } - } -} diff --git a/src/mcp/movie-reviews-mcp/package.json b/src/mcp/movie-reviews-mcp/package.json deleted file mode 100644 index d695836..0000000 --- a/src/mcp/movie-reviews-mcp/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "movie-reviews-mcp", - "version": "1.0.0", - "main": "index.js", - "type": "module", - "scripts": { - - "build": "tsc && chmod 755 build/index.js" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.1", - "zod": "^3.25.76" - }, - "devDependencies": { - "@types/node": "^24.5.2", - "typescript": "^5.9.2" - } -} diff --git a/src/mcp/movie-reviews-mcp/pyproject.toml b/src/mcp/movie-reviews-mcp/pyproject.toml new file mode 100644 index 0000000..c024fdf --- /dev/null +++ b/src/mcp/movie-reviews-mcp/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "attractions-mcp" +version = "1.0.0" +description = "MCP server for discovering and booking tourist attractions worldwide" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mcp[cli]>=1.13.1", + "requests", +] diff --git a/src/mcp/movie-reviews-mcp/readme.md b/src/mcp/movie-reviews-mcp/readme.md index 7c8a74a..5af9573 100644 --- a/src/mcp/movie-reviews-mcp/readme.md +++ b/src/mcp/movie-reviews-mcp/readme.md @@ -1,16 +1,161 @@ +# Tourist Attractions MCP Server -## Setup +A Model Context Protocol (MCP) server for discovering and booking tourist attractions worldwide using the World Tourist Attractions API. -1. Install Claud Desktop: https://claude.ai/download -2. Add `claude_desktop_config.json` file in install folder (likely `C:\Users\\AppData\Local\AnthropicClaude`) with the following contents (replace folder location): +## Features -```json -{ - "mcpServers": { - "weather": { - "command": "node", - "args": ["C:\\\\Hackathon-2025\\src\\mcp\\movie-reviews-mcp\build\\index.js"] - } - } -} +🏛️ **Attraction Discovery** +- Search attractions by location and category +- Get detailed attraction information +- Discover random famous attractions +- Explore world wonders + +🎫 **Booking System** +- Book attraction visits +- Generate confirmation codes +- Calculate estimated costs +- Validate visit dates and requirements + +🗂️ **Categories Supported** +- Historical Sites +- Natural Wonders +- Cultural Sites +- Religious Sites +- Museums +- Parks & Gardens +- Beaches & Mountains +- Architecture +- Entertainment & Adventure Sports + +## Installation + +```bash +## cd to attractions mcp project +cd src/mcp/attractions-mcp +``` + +```bash +# Install dependencies +uv sync +``` + +### Running the Server + +```bash +uv run mcp dev main.py +``` + +```bash +# use this when wanting to consume within an agent +uv run main.py +``` + +### Available Tools + +#### 1. Get Attraction Details +```python +get_attraction_details(attraction_id: int) +``` +Get comprehensive information about a specific attraction including facilities, best visiting times, and reviews. + +#### 2. Search Attractions +```python +search_attractions(location: str = None, category: str = None, limit: int = 20) +``` +Search for attractions with optional location and category filters. + +#### 3. Random Attraction Discovery +```python +get_random_attraction(region: str = "famous") +``` +Get a random attraction for inspiration. Use `region="india"` for Indian attractions. + +#### 4. World Wonders +```python +get_world_wonders() +``` +Get the list of world wonder attractions. + +#### 5. Book Attraction +```python +book_attraction( + attraction_id: int, + visitor_name: str, + email: str, + visit_date: str, # YYYY-MM-DD format + num_visitors: int = 1, + phone: str = None, + special_requirements: str = None +) +``` +Book a visit to an attraction with confirmation. + +#### 6. Get Categories +```python +get_attraction_categories() ``` +Get all available attraction categories for filtering. + +#### 7. Search and Format +```python +search_and_format_attractions(location: str = None, category: str = None, limit: int = 10) +``` +Search attractions and return nicely formatted results. + +### Resources + +Access attraction data as resources: +- `attraction://{attraction_id}` - Specific attraction details +- `attractions://search/{location}` - Attractions by location +- `attractions://category/{category}` - Attractions by category + +### Prompts + +- `attraction_booking_prompt(location, category)` - Booking assistance +- `travel_planning_prompt(location, days)` - Multi-day itinerary planning +- `attraction_comparison_prompt(attraction_ids)` - Compare multiple attractions + +## Example Usage + +```python +# Search for historical attractions in Rome +search_attractions(location="Rome", category="historical", limit=10) + +# Get details about the Colosseum (example ID: 123) +get_attraction_details(123) + +# Book a visit +book_attraction( + attraction_id=123, + visitor_name="John Doe", + email="john@example.com", + visit_date="2024-06-15", + num_visitors=2 +) + +# Get formatted search results +search_and_format_attractions(location="Paris", category="cultural", limit=5) +``` + +## Project Structure + +``` +src/mcp/attractions-mcp/ +├── __init__.py # Package exports +├── main.py # MCP server setup and tools +├── models.py # Data classes (Attraction, Booking, etc.) +├── config.py # API URLs and constants +├── utils.py # Helper functions and validation +├── attractions_service.py # Core business logic +├── pyproject.toml # Dependencies +└── README.md # This file +``` + +## Development + +The codebase follows a modular structure similar to the weather-mcp: +- **Models**: Data structures for attractions and bookings +- **Config**: Mock Data for attractions +- **Utils**: Helper functions for API calls and validation +- **Service**: Business logic and data processing +- **Main**: MCP server orchestration diff --git a/src/mcp/movie-reviews-mcp/src/index.ts b/src/mcp/movie-reviews-mcp/src/index.ts deleted file mode 100644 index 5860904..0000000 --- a/src/mcp/movie-reviews-mcp/src/index.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; - -const NWS_API_BASE = "https://api.weather.gov"; -const USER_AGENT = "weather-app/1.0"; - -// Create server instance -const server = new McpServer({ - name: "weather", - version: "1.0.0", - capabilities: { - resources: {}, - tools: {}, - }, -}); - -// Helper function for making NWS API requests -async function makeNWSRequest(url: string): Promise { - const headers = { - "User-Agent": USER_AGENT, - Accept: "application/geo+json", - }; - - try { - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return (await response.json()) as T; - } catch (error) { - console.error("Error making NWS request:", error); - return null; - } -} - -interface AlertFeature { - properties: { - event?: string; - areaDesc?: string; - severity?: string; - status?: string; - headline?: string; - }; -} - -// Format alert data -function formatAlert(feature: AlertFeature): string { - const props = feature.properties; - return [ - `Event: ${props.event || "Unknown"}`, - `Area: ${props.areaDesc || "Unknown"}`, - `Severity: ${props.severity || "Unknown"}`, - `Status: ${props.status || "Unknown"}`, - `Headline: ${props.headline || "No headline"}`, - "---", - ].join("\n"); -} - -interface ForecastPeriod { - name?: string; - temperature?: number; - temperatureUnit?: string; - windSpeed?: string; - windDirection?: string; - shortForecast?: string; -} - -interface AlertsResponse { - features: AlertFeature[]; -} - -interface PointsResponse { - properties: { - forecast?: string; - }; -} - -interface ForecastResponse { - properties: { - periods: ForecastPeriod[]; - }; -} - -// Register weather tools -server.tool( - "get_alerts", - "Get weather alerts for a state", - { - state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"), - }, - async ({ state }) => { - const stateCode = state.toUpperCase(); - const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; - const alertsData = await makeNWSRequest(alertsUrl); - - if (!alertsData) { - return { - content: [ - { - type: "text", - text: "Failed to retrieve alerts data", - }, - ], - }; - } - - const features = alertsData.features || []; - if (features.length === 0) { - return { - content: [ - { - type: "text", - text: `No active alerts for ${stateCode}`, - }, - ], - }; - } - - const formattedAlerts = features.map(formatAlert); - const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`; - - return { - content: [ - { - type: "text", - text: alertsText, - }, - ], - }; - }, -); - -server.tool( - "get_forecast", - "Get weather forecast for a location", - { - latitude: z.number().min(-90).max(90).describe("Latitude of the location"), - longitude: z - .number() - .min(-180) - .max(180) - .describe("Longitude of the location"), - }, - async ({ latitude, longitude }) => { - // Get grid point data - const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; - const pointsData = await makeNWSRequest(pointsUrl); - - if (!pointsData) { - return { - content: [ - { - type: "text", - text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, - }, - ], - }; - } - - const forecastUrl = pointsData.properties?.forecast; - if (!forecastUrl) { - return { - content: [ - { - type: "text", - text: "Failed to get forecast URL from grid point data", - }, - ], - }; - } - - // Get forecast data - const forecastData = await makeNWSRequest(forecastUrl); - if (!forecastData) { - return { - content: [ - { - type: "text", - text: "Failed to retrieve forecast data", - }, - ], - }; - } - - const periods = forecastData.properties?.periods || []; - if (periods.length === 0) { - return { - content: [ - { - type: "text", - text: "No forecast periods available", - }, - ], - }; - } - - // Format forecast periods - const formattedForecast = periods.map((period: ForecastPeriod) => - [ - `${period.name || "Unknown"}:`, - `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`, - `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`, - `${period.shortForecast || "No forecast available"}`, - "---", - ].join("\n"), - ); - - const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`; - - return { - content: [ - { - type: "text", - text: forecastText, - }, - ], - }; - }, -); - -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Weather MCP Server running on stdio"); -} - -main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); -}); diff --git a/src/mcp/movie-reviews-mcp/tsconfig.json b/src/mcp/movie-reviews-mcp/tsconfig.json deleted file mode 100644 index 28933fb..0000000 --- a/src/mcp/movie-reviews-mcp/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", - "outDir": "./build", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py new file mode 100644 index 0000000..14595be --- /dev/null +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -0,0 +1,214 @@ +""" +Utility functions for tourist attractions operations. +""" + +import requests +import random +import string +from typing import Dict, Any, Optional, List +from datetime import datetime, timedelta + +from config import ATTRACTIONS_BASE_URL, ENDPOINTS, ATTRACTION_CATEGORIES, MOCK_ATTRACTIONS, WORLD_WONDERS +from models import Coordinates, Location, Attraction + + +def make_api_request(url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Make an API request with error handling""" + try: + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"API request failed: {str(e)}") + + +def get_attraction_by_id(attraction_id: int) -> Optional[Dict[str, Any]]: + """Get attraction details by ID from mock data""" + for attraction in MOCK_ATTRACTIONS: + if attraction["id"] == attraction_id: + return attraction + return None + + +def search_attractions( + location: Optional[str] = None, + category: Optional[str] = None, + limit: int = 20 +) -> Optional[Dict[str, Any]]: + """Search for attractions with filters using mock data""" + filtered_attractions = [] + + for attraction in MOCK_ATTRACTIONS: + # Filter by location (check city, country, or region) + location_match = True + if location: + location_lower = location.lower() + attr_location = attraction["location"] + location_match = ( + location_lower in attr_location.get("city", "").lower() or + location_lower in attr_location.get("country", "").lower() or + location_lower in attr_location.get("region", "").lower() + ) + + # Filter by category + category_match = True + if category: + category_match = attraction["category"] == category.lower() + + if location_match and category_match: + filtered_attractions.append(attraction) + + # Apply limit + filtered_attractions = filtered_attractions[:limit] + + return { + "attractions": filtered_attractions, + "total": len(filtered_attractions) + } + + +def get_random_famous_attraction() -> Optional[Dict[str, Any]]: + """Get a random famous attraction from mock data""" + if MOCK_ATTRACTIONS: + return random.choice(MOCK_ATTRACTIONS) + return None + + +def get_random_india_attraction() -> Optional[Dict[str, Any]]: + """Get a random tourist attraction in India from mock data""" + indian_attractions = [attr for attr in MOCK_ATTRACTIONS if attr["location"]["country"] == "India"] + if indian_attractions: + return random.choice(indian_attractions) + return None + + +def get_wonders_of_world() -> Optional[Dict[str, Any]]: + """Get wonders of the world attractions from mock data""" + wonders = [attr for attr in MOCK_ATTRACTIONS if attr["id"] in WORLD_WONDERS] + return { + "attractions": wonders, + "total": len(wonders) + } + + +def parse_coordinates(lat: float, lon: float) -> Coordinates: + """Create coordinates object from lat/lon""" + return Coordinates(lat=lat, lon=lon) + + +def parse_location_data(data: Dict[str, Any]) -> Location: + """Parse location data from API response""" + coords = None + if data.get("latitude") and data.get("longitude"): + coords = parse_coordinates(data["latitude"], data["longitude"]) + + return Location( + city=data.get("city", ""), + country=data.get("country", ""), + region=data.get("region", ""), + coordinates=coords + ) + + +def parse_attraction_data(data: Dict[str, Any]) -> Attraction: + """Parse attraction data from API response""" + location = parse_location_data(data.get("location", {})) + + return Attraction( + id=data.get("id", 0), + name=data.get("name", ""), + description=data.get("description", ""), + category=data.get("category", ""), + location=location, + rating=data.get("rating"), + image_url=data.get("image_url"), + website=data.get("website"), + opening_hours=data.get("opening_hours"), + entry_fee=data.get("entry_fee") + ) + + +def format_attraction_name(attraction: Attraction) -> str: + """Format attraction name with location""" + name = attraction.name + if attraction.location.city: + name += f", {attraction.location.city}" + if attraction.location.country: + name += f", {attraction.location.country}" + return name + + +def get_category_display_name(category: str) -> str: + """Get display name for category""" + return ATTRACTION_CATEGORIES.get(category.lower(), category.title()) + + +def generate_booking_id() -> str: + """Generate a unique booking ID""" + timestamp = datetime.now().strftime("%Y%m%d%H%M") + random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + return f"ATT-{timestamp}-{random_part}" + + +def generate_confirmation_code() -> str: + """Generate a confirmation code""" + return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + + +def validate_visit_date(date_str: str) -> bool: + """Validate that visit date is in the future""" + try: + visit_date = datetime.strptime(date_str, "%Y-%m-%d") + return visit_date.date() > datetime.now().date() + except ValueError: + return False + + +def validate_email(email: str) -> bool: + """Basic email validation""" + return "@" in email and "." in email.split("@")[1] + + +def calculate_estimated_cost(num_visitors: int, entry_fee: Optional[str] = None) -> Optional[float]: + """Calculate estimated cost based on number of visitors""" + if not entry_fee or "free" in entry_fee.lower(): + return 0.0 + + # Simple cost calculation - in reality this would be more complex + try: + # Extract number from entry fee string (e.g., "$15", "₹500") + import re + numbers = re.findall(r'\d+\.?\d*', entry_fee) + if numbers: + base_cost = float(numbers[0]) + return base_cost * num_visitors + except (ValueError, IndexError): + pass + + return None + + +def format_attraction_details(attraction: Attraction) -> str: + """Format attraction details as a readable string""" + location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country + + details = f"🏛️ {attraction.name}\n" + details += f"📍 Location: {location_str}\n" + details += f"🏷️ Category: {get_category_display_name(attraction.category)}\n" + + if attraction.rating: + details += f"⭐ Rating: {attraction.rating}/5.0\n" + + if attraction.opening_hours: + details += f"🕒 Hours: {attraction.opening_hours}\n" + + if attraction.entry_fee: + details += f"💰 Entry Fee: {attraction.entry_fee}\n" + + if attraction.description: + details += f"\n📝 Description: {attraction.description}\n" + + if attraction.website: + details += f"🌐 Website: {attraction.website}\n" + + return details diff --git a/src/mcp/movie-reviews-mcp/uv.lock b/src/mcp/movie-reviews-mcp/uv.lock new file mode 100644 index 0000000..0ada2fd --- /dev/null +++ b/src/mcp/movie-reviews-mcp/uv.lock @@ -0,0 +1,545 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "attractions-mcp" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }, + { name = "requests" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/3c/82c400c2d50afdac4fbefb5b4031fd327e2ad1f23ccef8eee13c5909aa48/mcp-1.13.1.tar.gz", hash = "sha256:165306a8fd7991dc80334edd2de07798175a56461043b7ae907b279794a834c5", size = 438198, upload-time = "2025-08-22T09:22:16.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/3f/d085c7f49ade6d273b185d61ec9405e672b6433f710ea64a90135a8dd445/mcp-1.13.1-py3-none-any.whl", hash = "sha256:c314e7c8bd477a23ba3ef472ee5a32880316c42d03e06dcfa31a1cc7a73b65df", size = 161494, upload-time = "2025-08-22T09:22:14.705Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, +] + +[[package]] +name = "typer" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/82/f4bfed3bc18c6ebd6f828320811bbe4098f92a31adf4040bee59c4ae02ea/typer-0.17.3.tar.gz", hash = "sha256:0c600503d472bcf98d29914d4dcd67f80c24cc245395e2e00ba3603c9332e8ba", size = 103517, upload-time = "2025-08-30T12:35:24.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/e8/b3d537470e8404659a6335e7af868e90657efb73916ef31ddf3d8b9cb237/typer-0.17.3-py3-none-any.whl", hash = "sha256:643919a79182ab7ac7581056d93c6a2b865b026adf2872c4d02c72758e6f095b", size = 46494, upload-time = "2025-08-30T12:35:22.391Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] From e8362f7782f24bc9ab8efb3882a91f22e04e58a3 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:24:52 +0100 Subject: [PATCH 07/25] Delete .python-version --- src/mcp/cinema-mcp/.python-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/mcp/cinema-mcp/.python-version diff --git a/src/mcp/cinema-mcp/.python-version b/src/mcp/cinema-mcp/.python-version deleted file mode 100644 index e4fba21..0000000 --- a/src/mcp/cinema-mcp/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 From ae40b748a1aa8566bc2430d698e96e383c34d196 Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 12:48:34 +0100 Subject: [PATCH 08/25] Start fleshing out movie service --- src/mcp/movie-reviews-mcp/config.py | 311 +++++++----------- src/mcp/movie-reviews-mcp/models.py | 89 +---- ...ttractions_service.py => movie_service.py} | 115 ++++--- src/mcp/movie-reviews-mcp/utils.py | 161 ++++----- 4 files changed, 267 insertions(+), 409 deletions(-) rename src/mcp/movie-reviews-mcp/{attractions_service.py => movie_service.py} (94%) diff --git a/src/mcp/movie-reviews-mcp/config.py b/src/mcp/movie-reviews-mcp/config.py index 7500597..0742b6b 100644 --- a/src/mcp/movie-reviews-mcp/config.py +++ b/src/mcp/movie-reviews-mcp/config.py @@ -2,49 +2,17 @@ Tourist attractions MCP configuration mocks data. """ -# World Tourist Attractions API configuration -ATTRACTIONS_BASE_URL = "https://www.world-tourist-attractions-api.com" -API_VERSION = "v1" - -# API Endpoints -ENDPOINTS = { - "attraction_by_id": f"/api/{API_VERSION}/attraction", - "random_famous": f"/api/{API_VERSION}/random/famous", - "random_india": f"/api/{API_VERSION}/random/india", - "wonders": f"/api/{API_VERSION}/wonders", - "search": f"/api/{API_VERSION}/search", - "categories": f"/api/{API_VERSION}/categories" -} - -# Attraction categories -ATTRACTION_CATEGORIES = { - "historical": "Historical Sites", - "natural": "Natural Wonders", - "cultural": "Cultural Sites", - "religious": "Religious Sites", - "modern": "Modern Attractions", - "museums": "Museums", - "parks": "Parks & Gardens", - "beaches": "Beaches", - "mountains": "Mountains", - "architecture": "Architecture", - "entertainment": "Entertainment", - "adventure": "Adventure Sports" -} - -# Popular countries/regions -POPULAR_REGIONS = [ - "India", "France", "Italy", "Spain", "Greece", "Egypt", - "Thailand", "Japan", "China", "USA", "UK", "Germany", - "Turkey", "Morocco", "Brazil", "Peru", "Australia" -] - -# Booking status codes -BOOKING_STATUS = { - "pending": "Pending Confirmation", - "confirmed": "Confirmed", - "cancelled": "Cancelled", - "completed": "Completed" +MOVIE_GENRES = { + "action": "Action", + "comedy": "Comedy", + "drama": "Drama", + "horror": "Horror", + "romance": "Romance", + "thriller": "Thriller", + "sci-fi": "Science Fiction", + "fantasy": "Fantasy", + "documentary": "Documentary", + "animation": "Animation" } # Default values @@ -53,188 +21,165 @@ DEFAULT_RATING_MIN = 3.0 # Mock attractions data for demonstration -MOCK_ATTRACTIONS = [ +MOCK_MOVIES = [ { "id": 1, - "name": "Eiffel Tower", - "description": "Iconic iron lattice tower located on the Champ de Mars in Paris, France", - "category": "architecture", - "location": {"city": "Paris", "country": "France", "region": "Île-de-France"}, - "rating": 4.6, - "image_url": "https://example.com/eiffel-tower.jpg", - "website": "https://www.toureiffel.paris", - "opening_hours": "9:30 AM - 11:45 PM", - "entry_fee": "€29.40 - €73.30" + "title": "Inception", + "synopsis": "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.", + "rating": 8.8, + "durationMins": 148, + "genre": "sci-fi" }, { "id": 2, - "name": "Taj Mahal", - "description": "Ivory-white marble mausoleum on the right bank of the river Yamuna in Agra", - "category": "historical", - "location": {"city": "Agra", "country": "India", "region": "Uttar Pradesh"}, - "rating": 4.8, - "image_url": "https://example.com/taj-mahal.jpg", - "website": "https://www.tajmahal.gov.in", - "opening_hours": "6:00 AM - 7:00 PM", - "entry_fee": "₹1100 (foreigners), ₹50 (Indians)" + "title": "The Godfather", + "synopsis": "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", + "rating": 9.2, + "durationMins": 175, + "genre": "drama" }, { "id": 3, - "name": "Colosseum", - "description": "Ancient Roman amphitheater in the center of Rome, Italy", - "category": "historical", - "location": {"city": "Rome", "country": "Italy", "region": "Lazio"}, - "rating": 4.5, - "image_url": "https://example.com/colosseum.jpg", - "website": "https://www.coopculture.it", - "opening_hours": "8:30 AM - 7:15 PM", - "entry_fee": "€16 - €22" + "title": "The Dark Knight", + "synopsis": "When the menace known as the Joker emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham. The Dark Knight must accept one of the greatest psychological and physical tests of his ability to fight injustice.", + "rating": 9.0, + "durationMins": 152, + "genre": "action" }, { "id": 4, - "name": "Machu Picchu", - "description": "Ancient Incan city set high in the Andes Mountains of Peru", - "category": "historical", - "location": {"city": "Cusco", "country": "Peru", "region": "Cusco"}, - "rating": 4.9, - "image_url": "https://example.com/machu-picchu.jpg", - "website": "https://www.machupicchu.gob.pe", - "opening_hours": "6:00 AM - 5:30 PM", - "entry_fee": "$47 - $62" + "title": "Pulp Fiction", + "synopsis": "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", + "rating": 8.9, + "durationMins": 154, + "genre": "crime" }, { "id": 5, - "name": "Louvre Museum", - "description": "World's largest art museum and historic monument in Paris", - "category": "museums", - "location": {"city": "Paris", "country": "France", "region": "Île-de-France"}, - "rating": 4.4, - "image_url": "https://example.com/louvre.jpg", - "website": "https://www.louvre.fr", - "opening_hours": "9:00 AM - 6:00 PM", - "entry_fee": "€17" + "title": "Forrest Gump", + "synopsis": "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with a low IQ.", + "rating": 8.8, + "durationMins": 142, + "genre": "drama" }, { "id": 6, - "name": "Great Wall of China", - "description": "Ancient fortification built across northern China", - "category": "historical", - "location": {"city": "Beijing", "country": "China", "region": "Beijing"}, - "rating": 4.7, - "image_url": "https://example.com/great-wall.jpg", - "website": "https://www.mutianyu.com", - "opening_hours": "7:30 AM - 5:30 PM", - "entry_fee": "¥45 - ¥65" + "title": "Interstellar", + "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", + "rating": 8.6, + "durationMins": 169, + "genre": "sci-fi" }, { "id": 7, - "name": "Santorini", - "description": "Beautiful Greek island with white buildings and blue domes", - "category": "natural", - "location": {"city": "Santorini", "country": "Greece", "region": "Cyclades"}, - "rating": 4.6, - "image_url": "https://example.com/santorini.jpg", - "website": "https://www.santorini.com", - "opening_hours": "24/7", - "entry_fee": "Free" + "title": "Parasite", + "synopsis": "Greed and class discrimination threaten the newly formed symbiotic relationship between the wealthy Park family and the destitute Kim clan.", + "rating": 8.6, + "durationMins": 132, + "genre": "thriller" }, { "id": 8, - "name": "Angkor Wat", - "description": "Largest religious monument in the world, originally a Hindu temple", - "category": "religious", - "location": {"city": "Siem Reap", "country": "Cambodia", "region": "Siem Reap"}, - "rating": 4.8, - "image_url": "https://example.com/angkor-wat.jpg", - "website": "https://www.angkorwat.com", - "opening_hours": "5:00 AM - 6:00 PM", - "entry_fee": "$37 (1 day), $62 (3 days)" + "title": "Schindler's List", + "synopsis": "In German-occupied Poland during World War II, industrialist Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", + "rating": 8.9, + "durationMins": 195, + "genre": "history" }, { "id": 9, - "name": "Central Park", - "description": "Large public park in Manhattan, New York City", - "category": "parks", - "location": {"city": "New York", "country": "USA", "region": "New York"}, - "rating": 4.3, - "image_url": "https://example.com/central-park.jpg", - "website": "https://www.centralparknyc.org", - "opening_hours": "6:00 AM - 1:00 AM", - "entry_fee": "Free" + "title": "The Shawshank Redemption", + "synopsis": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", + "rating": 9.3, + "durationMins": 142, + "genre": "drama" }, { "id": 10, - "name": "Petra", - "description": "Archaeological city famous for rock-cut architecture and water conduit system", - "category": "historical", - "location": {"city": "Ma'an", "country": "Jordan", "region": "Ma'an"}, - "rating": 4.7, - "image_url": "https://example.com/petra.jpg", - "website": "https://www.visitpetra.jo", - "opening_hours": "6:00 AM - 6:00 PM", - "entry_fee": "70 JOD (1 day), 55 JOD (2 days)" + "title": "Spirited Away", + "synopsis": "During her family's move to the suburbs, a sullen 10-year-old girl wanders into a world ruled by gods, witches, and spirits, where humans are changed into beasts.", + "rating": 8.6, + "durationMins": 125, + "genre": "animation" }, { "id": 11, - "name": "Statue of Liberty", - "description": "Neoclassical sculpture on Liberty Island in New York Harbor", - "category": "modern", - "location": {"city": "New York", "country": "USA", "region": "New York"}, - "rating": 4.4, - "image_url": "https://example.com/statue-liberty.jpg", - "website": "https://www.nps.gov/stli", - "opening_hours": "8:30 AM - 4:00 PM", - "entry_fee": "$23.80 - $24.30" + "title": "Gladiator", + "synopsis": "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", + "rating": 8.5, + "durationMins": 155, + "genre": "action" }, { "id": 12, - "name": "Sagrada Familia", - "description": "Unfinished Roman Catholic minor basilica in Barcelona, Spain", - "category": "religious", - "location": {"city": "Barcelona", "country": "Spain", "region": "Catalonia"}, - "rating": 4.6, - "image_url": "https://example.com/sagrada-familia.jpg", - "website": "https://sagradafamilia.org", - "opening_hours": "9:00 AM - 8:00 PM", - "entry_fee": "€26 - €40" + "title": "The Matrix", + "synopsis": "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", + "rating": 8.7, + "durationMins": 136, + "genre": "sci-fi" }, { "id": 13, - "name": "Kinkaku-ji", - "description": "Golden Pavilion, a Zen temple in Kyoto, Japan", - "category": "religious", - "location": {"city": "Kyoto", "country": "Japan", "region": "Kansai"}, - "rating": 4.5, - "image_url": "https://example.com/kinkaku-ji.jpg", - "website": "https://www.shokoku-ji.jp", - "opening_hours": "9:00 AM - 5:00 PM", - "entry_fee": "¥500" + "title": "Fight Club", + "synopsis": "An insomniac office worker and a devil-may-care soap maker form an underground fight club that evolves into something much more.", + "rating": 8.8, + "durationMins": 139, + "genre": "drama" }, { "id": 14, - "name": "Sydney Opera House", - "description": "Multi-venue performing arts center in Sydney, Australia", - "category": "modern", - "location": {"city": "Sydney", "country": "Australia", "region": "New South Wales"}, - "rating": 4.4, - "image_url": "https://example.com/sydney-opera.jpg", - "website": "https://www.sydneyoperahouse.com", - "opening_hours": "9:00 AM - 8:30 PM", - "entry_fee": "$43 - $175" + "title": "The Lord of the Rings: The Return of the King", + "synopsis": "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", + "rating": 8.9, + "durationMins": 201, + "genre": "fantasy" }, { "id": 15, - "name": "Christ the Redeemer", - "description": "Art Deco statue of Jesus Christ in Rio de Janeiro, Brazil", - "category": "religious", - "location": {"city": "Rio de Janeiro", "country": "Brazil", "region": "Rio de Janeiro"}, - "rating": 4.5, - "image_url": "https://example.com/christ-redeemer.jpg", - "website": "https://www.cristoredentor.com.br", - "opening_hours": "8:00 AM - 7:00 PM", - "entry_fee": "R$65 - R$98" + "title": "The Lion King", + "synopsis": "Lion prince Simba and his father are targeted by his bitter uncle, who wants to ascend the throne himself.", + "rating": 8.5, + "durationMins": 88, + "genre": "animation" + }, + { + "id": 16, + "title": "Goodfellas", + "synopsis": "The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen Hill and his mob partners.", + "rating": 8.7, + "durationMins": 146, + "genre": "crime" + }, + { + "id": 17, + "title": "The Silence of the Lambs", + "synopsis": "A young F.B.I. cadet must receive the help of an incarcerated and manipulative cannibal killer to catch another serial killer.", + "rating": 8.6, + "durationMins": 118, + "genre": "thriller" + }, + { + "id": 18, + "title": "Saving Private Ryan", + "synopsis": "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", + "rating": 8.6, + "durationMins": 169, + "genre": "war" + }, + { + "id": 19, + "title": "The Prestige", + "synopsis": "After a tragic accident, two stage magicians engage in a battle to create the ultimate illusion while sacrificing everything they have to outwit each other.", + "rating": 8.5, + "durationMins": 130, + "genre": "mystery" + }, + { + "id": 20, + "title": "Whiplash", + "synopsis": "A promising young drummer enrolls at a cut-throat music conservatory where his dreams of greatness are mentored by an instructor who will stop at nothing to realize a student's potential.", + "rating": 8.5, + "durationMins": 106, + "genre": "drama" } -] - -# World wonders mock data -WORLD_WONDERS = [1, 2, 3, 4, 6, 8, 10, 15] # IDs from MOCK_ATTRACTIONS that are world wonders \ No newline at end of file +] \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/models.py b/src/mcp/movie-reviews-mcp/models.py index 920cc27..babbd31 100644 --- a/src/mcp/movie-reviews-mcp/models.py +++ b/src/mcp/movie-reviews-mcp/models.py @@ -1,82 +1,27 @@ """ -Tourist attractions data models - Data classes for attractions objects. +Movie reviews and information models """ from dataclasses import dataclass from typing import Optional, List +from datetime import datetime - +# movie reviews @dataclass -class Coordinates: - lat: float - lon: float - - -@dataclass -class Location: - city: str - country: str - region: Optional[str] = "" - coordinates: Optional[Coordinates] = None - +class MovieReview: + movieId: int + rating: int + review: str + reviewDate: datetime +# movie @dataclass -class Attraction: +class Movie: id: int - name: str - description: str - category: str - location: Location - rating: Optional[float] = None - image_url: Optional[str] = None - website: Optional[str] = None - opening_hours: Optional[str] = None - entry_fee: Optional[str] = None - - -@dataclass -class AttractionDetails: - attraction: Attraction - reviews_count: Optional[int] = None - facilities: Optional[List[str]] = None - best_time_to_visit: Optional[str] = None - duration: Optional[str] = None - - -@dataclass -class BookingRequest: - attraction_id: int - visitor_name: str - email: str - visit_date: str - num_visitors: int = 1 - phone: Optional[str] = None - special_requirements: Optional[str] = None - - -@dataclass -class BookingResponse: - booking_id: str - attraction_id: int - visitor_name: str - visit_date: str - num_visitors: int - total_cost: Optional[float] = None - booking_status: str = "confirmed" - confirmation_code: Optional[str] = None - - -@dataclass -class AttractionsList: - category: str - location: Optional[str] = None - total_count: int = 0 - attractions: List[Attraction] = None - - -@dataclass -class SearchFilters: - location: Optional[str] = None - category: Optional[str] = None - rating_min: Optional[float] = None - free_entry: Optional[bool] = None + title: str + synopsis: str + rating: float + durationMins: int + year: int + genres: List[str] + reviews: List[MovieReview] diff --git a/src/mcp/movie-reviews-mcp/attractions_service.py b/src/mcp/movie-reviews-mcp/movie_service.py similarity index 94% rename from src/mcp/movie-reviews-mcp/attractions_service.py rename to src/mcp/movie-reviews-mcp/movie_service.py index 57bcdab..44aff0f 100644 --- a/src/mcp/movie-reviews-mcp/attractions_service.py +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -1,5 +1,5 @@ """ -Attractions service with MCP tools and API logic. +Movies service with MCP tools and API logic. """ from typing import Dict, Any, List @@ -8,8 +8,7 @@ from config import DEFAULT_SEARCH_LIMIT, ATTRACTION_CATEGORIES from models import ( - AttractionDetails, BookingRequest, BookingResponse, - AttractionsList, SearchFilters + Movie, MovieReview ) from utils import ( get_attraction_by_id, search_attractions, get_random_famous_attraction, @@ -22,10 +21,10 @@ def get_attraction_details_data(attraction_id: int) -> Dict[str, Any]: """Get detailed information about a specific attraction - + Args: attraction_id: Unique ID of the attraction - + Returns: AttractionDetails object as dictionary or error dict """ @@ -33,9 +32,9 @@ def get_attraction_details_data(attraction_id: int) -> Dict[str, Any]: data = get_attraction_by_id(attraction_id) if not data: return {"error": f"Attraction with ID {attraction_id} not found"} - + attraction = parse_attraction_data(data) - + attraction_details = AttractionDetails( attraction=attraction, reviews_count=data.get("reviews_count"), @@ -43,25 +42,25 @@ def get_attraction_details_data(attraction_id: int) -> Dict[str, Any]: best_time_to_visit=data.get("best_time_to_visit"), duration=data.get("duration") ) - + return asdict(attraction_details) - + except Exception as e: return {"error": f"Failed to get attraction details: {str(e)}"} def search_attractions_data( - location: str = None, - category: str = None, + location: str = None, + category: str = None, limit: int = DEFAULT_SEARCH_LIMIT ) -> Dict[str, Any]: """Search for attractions with filters - + Args: location: Location to search in (e.g., "Paris", "India", "Italy") category: Category of attractions (e.g., "historical", "natural", "cultural") limit: Maximum number of results (default: 20, max: 100) - + Returns: AttractionsList object as dictionary or error dict """ @@ -70,36 +69,36 @@ def search_attractions_data( limit = 100 elif limit < 1: limit = 1 - + data = search_attractions(location, category, limit) if not data: return {"error": "No attractions found matching the criteria"} - + attractions = [] if "attractions" in data: for item in data["attractions"]: attraction = parse_attraction_data(item) attractions.append(attraction) - + attractions_list = AttractionsList( category=get_category_display_name(category) if category else "All Categories", location=location or "Worldwide", total_count=data.get("total", len(attractions)), attractions=attractions ) - + return asdict(attractions_list) - + except Exception as e: return {"error": f"Failed to search attractions: {str(e)}"} def get_random_attraction_data(region: str = "famous") -> Dict[str, Any]: """Get a random attraction - + Args: region: Region type - "famous" for world famous attractions, "india" for Indian attractions - + Returns: Attraction object as dictionary or error dict """ @@ -108,20 +107,20 @@ def get_random_attraction_data(region: str = "famous") -> Dict[str, Any]: data = get_random_india_attraction() else: data = get_random_famous_attraction() - + if not data: return {"error": f"No random attraction found for region: {region}"} - + attraction = parse_attraction_data(data) return asdict(attraction) - + except Exception as e: return {"error": f"Failed to get random attraction: {str(e)}"} def get_world_wonders_data() -> str: """Get list of world wonders attractions as formatted string - + Returns: Formatted string with world wonders attractions """ @@ -129,34 +128,34 @@ def get_world_wonders_data() -> str: data = get_wonders_of_world() if not data: return "Error: No world wonders found" - + attractions = [] if "attractions" in data: for item in data["attractions"]: attraction = parse_attraction_data(item) attractions.append(attraction) - + result = "🌟 **Wonders of the World**\n\n" result += f"Total Wonders: {len(attractions)}\n\n" - + for i, attraction in enumerate(attractions, 1): location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country - + result += f"{i}. 🏛️ **{attraction.name}**\n" result += f" 📍 {location_str}\n" result += f" 🏷️ {get_category_display_name(attraction.category)}\n" - + if attraction.rating: result += f" ⭐ {attraction.rating}/5.0\n" if attraction.entry_fee: result += f" 💰 {attraction.entry_fee}\n" if attraction.description: result += f" 📝 {attraction.description}\n" - + result += "\n" - + return result - + except Exception as e: return f"Error: Failed to get world wonders: {str(e)}" @@ -171,7 +170,7 @@ def book_attraction_data( special_requirements: str = None ) -> Dict[str, Any]: """Book an attraction visit - + Args: attraction_id: ID of the attraction to book visitor_name: Name of the primary visitor @@ -180,37 +179,37 @@ def book_attraction_data( num_visitors: Number of visitors (default: 1) phone: Optional phone number special_requirements: Optional special requirements - + Returns: BookingResponse object as dictionary or error dict """ try: if not visitor_name.strip(): return {"error": "Visitor name is required"} - + if not validate_email(email): return {"error": "Invalid email address"} - + if not validate_visit_date(visit_date): return {"error": "Visit date must be in the future and in YYYY-MM-DD format"} - + if num_visitors < 1 or num_visitors > 50: return {"error": "Number of visitors must be between 1 and 50"} - + # Get attraction details for cost calculation attraction_data = get_attraction_by_id(attraction_id) if not attraction_data: return {"error": f"Attraction with ID {attraction_id} not found"} - + attraction = parse_attraction_data(attraction_data) - + # Calculate estimated cost total_cost = calculate_estimated_cost(num_visitors, attraction.entry_fee) - + # Create booking booking_id = generate_booking_id() confirmation_code = generate_confirmation_code() - + booking_response = BookingResponse( booking_id=booking_id, attraction_id=attraction_id, @@ -221,29 +220,29 @@ def book_attraction_data( booking_status="confirmed", confirmation_code=confirmation_code ) - + return asdict(booking_response) - + except Exception as e: return {"error": f"Failed to book attraction: {str(e)}"} def get_attraction_categories_data() -> str: """Get list of available attraction categories as formatted string - + Returns: Formatted string with category codes and display names """ try: result = "🏛️ **Available Attraction Categories**\n\n" result += f"Total Categories: {len(ATTRACTION_CATEGORIES)}\n\n" - + for code, display_name in ATTRACTION_CATEGORIES.items(): result += f"• **{code}**: {display_name}\n" - + result += "\n*Use these category codes when searching for attractions.*" return result - + except Exception as e: return f"Error: Failed to get categories: {str(e)}" @@ -253,10 +252,10 @@ def format_attraction_resource(attraction_id: int) -> str: data = get_attraction_details_data(attraction_id) if "error" in data: return f"Error: {data['error']}" - + attraction_data = data['attraction'] attraction = parse_attraction_data(attraction_data) - + return format_attraction_details(attraction) @@ -273,34 +272,34 @@ def format_search_results(search_data: Dict[str, Any]) -> str: """Format search results as a readable string""" if "error" in search_data: return f"Error: {search_data['error']}" - + attractions = search_data.get('attractions', []) if not attractions: return "No attractions found matching your criteria." - + result = f"🎯 Found {search_data.get('total_count', len(attractions))} attractions" if search_data.get('location') != "Worldwide": result += f" in {search_data['location']}" if search_data.get('category') != "All Categories": result += f" ({search_data['category']})" result += ":\n\n" - + for i, attraction_data in enumerate(attractions[:10], 1): # Show first 10 attraction = parse_attraction_data(attraction_data) location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country - + result += f"{i}. 🏛️ **{attraction.name}**\n" result += f" 📍 {location_str}\n" result += f" 🏷️ {get_category_display_name(attraction.category)}\n" - + if attraction.rating: result += f" ⭐ {attraction.rating}/5.0\n" if attraction.entry_fee: result += f" 💰 {attraction.entry_fee}\n" - + result += "\n" - + if len(attractions) > 10: result += f"... and {len(attractions) - 10} more attractions\n" - + return result diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py index 14595be..ea29266 100644 --- a/src/mcp/movie-reviews-mcp/utils.py +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -1,5 +1,5 @@ """ -Utility functions for tourist attractions operations. +Utility functions for tourist movies operations. """ import requests @@ -8,7 +8,7 @@ from typing import Dict, Any, Optional, List from datetime import datetime, timedelta -from config import ATTRACTIONS_BASE_URL, ENDPOINTS, ATTRACTION_CATEGORIES, MOCK_ATTRACTIONS, WORLD_WONDERS +from config import MOVIE_GENRES, MOCK_MOVIES from models import Coordinates, Location, Attraction @@ -22,75 +22,44 @@ def make_api_request(url: str, params: Optional[Dict[str, Any]] = None) -> Dict[ raise Exception(f"API request failed: {str(e)}") -def get_attraction_by_id(attraction_id: int) -> Optional[Dict[str, Any]]: - """Get attraction details by ID from mock data""" - for attraction in MOCK_ATTRACTIONS: - if attraction["id"] == attraction_id: - return attraction +def get_movie_by_id(movie_id: int) -> Optional[Dict[str, Any]]: + """Get movie details by ID from mock data""" + for movie in MOCK_MOVIES: + if movie["id"] == movie_id: + return movie return None -def search_attractions( - location: Optional[str] = None, - category: Optional[str] = None, +def search_movies( + title: Optional[str] = None, + genre: Optional[str] = None, limit: int = 20 ) -> Optional[Dict[str, Any]]: - """Search for attractions with filters using mock data""" - filtered_attractions = [] - - for attraction in MOCK_ATTRACTIONS: - # Filter by location (check city, country, or region) - location_match = True - if location: - location_lower = location.lower() - attr_location = attraction["location"] - location_match = ( - location_lower in attr_location.get("city", "").lower() or - location_lower in attr_location.get("country", "").lower() or - location_lower in attr_location.get("region", "").lower() - ) - - # Filter by category - category_match = True - if category: - category_match = attraction["category"] == category.lower() - - if location_match and category_match: - filtered_attractions.append(attraction) - - # Apply limit - filtered_attractions = filtered_attractions[:limit] - - return { - "attractions": filtered_attractions, - "total": len(filtered_attractions) - } + """Search for movies with filters using mock data""" + filtered_movies = [] + for movie in MOCK_MOVIES: -def get_random_famous_attraction() -> Optional[Dict[str, Any]]: - """Get a random famous attraction from mock data""" - if MOCK_ATTRACTIONS: - return random.choice(MOCK_ATTRACTIONS) - return None - + # Filter by genre + genre_match = True + if genre: + genre_match = movie["genre"] == genre.lower() -def get_random_india_attraction() -> Optional[Dict[str, Any]]: - """Get a random tourist attraction in India from mock data""" - indian_attractions = [attr for attr in MOCK_ATTRACTIONS if attr["location"]["country"] == "India"] - if indian_attractions: - return random.choice(indian_attractions) - return None + if genre_match: + filtered_movies.append(movie) + # Apply limit + filtered_movies = filtered_movies[:limit] -def get_wonders_of_world() -> Optional[Dict[str, Any]]: - """Get wonders of the world attractions from mock data""" - wonders = [attr for attr in MOCK_ATTRACTIONS if attr["id"] in WORLD_WONDERS] return { - "attractions": wonders, - "total": len(wonders) + "movies": filtered_movies, + "total": len(filtered_movies) } + + + def parse_coordinates(lat: float, lon: float) -> Coordinates: """Create coordinates object from lat/lon""" return Coordinates(lat=lat, lon=lon) @@ -101,7 +70,7 @@ def parse_location_data(data: Dict[str, Any]) -> Location: coords = None if data.get("latitude") and data.get("longitude"): coords = parse_coordinates(data["latitude"], data["longitude"]) - + return Location( city=data.get("city", ""), country=data.get("country", ""), @@ -110,15 +79,15 @@ def parse_location_data(data: Dict[str, Any]) -> Location: ) -def parse_attraction_data(data: Dict[str, Any]) -> Attraction: - """Parse attraction data from API response""" +def parse_movie_data(data: Dict[str, Any]) -> Attraction: + """Parse movie data from API response""" location = parse_location_data(data.get("location", {})) - + return Attraction( id=data.get("id", 0), name=data.get("name", ""), description=data.get("description", ""), - category=data.get("category", ""), + genre=data.get("genre", ""), location=location, rating=data.get("rating"), image_url=data.get("image_url"), @@ -128,19 +97,19 @@ def parse_attraction_data(data: Dict[str, Any]) -> Attraction: ) -def format_attraction_name(attraction: Attraction) -> str: - """Format attraction name with location""" - name = attraction.name - if attraction.location.city: - name += f", {attraction.location.city}" - if attraction.location.country: - name += f", {attraction.location.country}" +def format_movie_name(movie: Attraction) -> str: + """Format movie name with location""" + name = movie.name + if movie.location.city: + name += f", {movie.location.city}" + if movie.location.country: + name += f", {movie.location.country}" return name -def get_category_display_name(category: str) -> str: - """Get display name for category""" - return ATTRACTION_CATEGORIES.get(category.lower(), category.title()) +def get_genre_display_name(genre: str) -> str: + """Get display name for genre""" + return ATTRACTION_CATEGORIES.get(genre.lower(), genre.title()) def generate_booking_id() -> str: @@ -173,7 +142,7 @@ def calculate_estimated_cost(num_visitors: int, entry_fee: Optional[str] = None) """Calculate estimated cost based on number of visitors""" if not entry_fee or "free" in entry_fee.lower(): return 0.0 - + # Simple cost calculation - in reality this would be more complex try: # Extract number from entry fee string (e.g., "$15", "₹500") @@ -184,31 +153,31 @@ def calculate_estimated_cost(num_visitors: int, entry_fee: Optional[str] = None) return base_cost * num_visitors except (ValueError, IndexError): pass - + return None -def format_attraction_details(attraction: Attraction) -> str: - """Format attraction details as a readable string""" - location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country - - details = f"🏛️ {attraction.name}\n" +def format_movie_details(movie: Attraction) -> str: + """Format movie details as a readable string""" + location_str = f"{movie.location.city}, {movie.location.country}" if movie.location.city else movie.location.country + + details = f"🏛️ {movie.name}\n" details += f"📍 Location: {location_str}\n" - details += f"🏷️ Category: {get_category_display_name(attraction.category)}\n" - - if attraction.rating: - details += f"⭐ Rating: {attraction.rating}/5.0\n" - - if attraction.opening_hours: - details += f"🕒 Hours: {attraction.opening_hours}\n" - - if attraction.entry_fee: - details += f"💰 Entry Fee: {attraction.entry_fee}\n" - - if attraction.description: - details += f"\n📝 Description: {attraction.description}\n" - - if attraction.website: - details += f"🌐 Website: {attraction.website}\n" - + details += f"🏷️ Category: {get_genre_display_name(movie.genre)}\n" + + if movie.rating: + details += f"⭐ Rating: {movie.rating}/5.0\n" + + if movie.opening_hours: + details += f"🕒 Hours: {movie.opening_hours}\n" + + if movie.entry_fee: + details += f"💰 Entry Fee: {movie.entry_fee}\n" + + if movie.description: + details += f"\n📝 Description: {movie.description}\n" + + if movie.website: + details += f"🌐 Website: {movie.website}\n" + return details From 0eb0076cb8e33b057f6624bef52d1ffc0757a72b Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:54:41 +0100 Subject: [PATCH 09/25] Fixes --- src/mcp/cinema-mcp/cinema_service.py | 62 +++++++++++++++++++++++++--- src/mcp/cinema-mcp/main.py | 21 ++++++++-- src/mcp/cinema-mcp/utils.py | 61 +++++++++++++++++++-------- 3 files changed, 118 insertions(+), 26 deletions(-) diff --git a/src/mcp/cinema-mcp/cinema_service.py b/src/mcp/cinema-mcp/cinema_service.py index 43d0123..c449d39 100644 --- a/src/mcp/cinema-mcp/cinema_service.py +++ b/src/mcp/cinema-mcp/cinema_service.py @@ -12,8 +12,8 @@ ) from models import MovieShowing, ContactInfo, MovieReservation from utils import ( - get_movie_by_id, search_movies_by_criteria, get_current_movies, - get_movies_by_date, format_movie_details, parse_movie_data + get_movie_by_id, search_movie_presentations, get_current_movies, + get_movies_by_date, search_movies_by_title, format_movie_details, parse_movie_data ) @@ -109,15 +109,17 @@ def get_movie_details_data(movie_id: str) -> Dict[str, Any]: def search_movies_data( + title: Optional[str] = None, genre: Optional[str] = None, date: Optional[str] = None, room: Optional[str] = None, available_seats_min: Optional[int] = None, limit: int = DEFAULT_SEARCH_LIMIT ) -> Dict[str, Any]: - """Search for movies with filters + """Search for movie presentations with filters Args: + title: Movie title to search for (partial match, case-insensitive) genre: Movie genre filter (e.g., "action", "comedy", "drama") date: Date filter in YYYY-MM-DD format room: Cinema room filter @@ -125,7 +127,7 @@ def search_movies_data( limit: Maximum number of results (default: 20, max: 100) Returns: - Filtered list of movies or error dict + Filtered list of movie presentations or error dict """ try: if limit > 100: @@ -141,7 +143,8 @@ def search_movies_data( except ValueError: return {"error": "Invalid date format. Use YYYY-MM-DD"} - movies = search_movies_by_criteria( + movies = search_movie_presentations( + title=title, genre=genre, date=date_filter, room=room, @@ -170,6 +173,8 @@ def search_movies_data( # Build filter summary filters_applied = [] + if title: + filters_applied.append(f"Title: {title}") if genre: filters_applied.append(f"Genre: {MOVIE_GENRES.get(genre, genre)}") if date: @@ -234,4 +239,49 @@ def get_movies_by_date_data(date: str) -> Dict[str, Any]: } except Exception as e: - return {"error": f"Failed to get movies for date: {str(e)}"} \ No newline at end of file + return {"error": f"Failed to get movies for date: {str(e)}"} + + +def search_movies_by_title_data(title: str) -> Dict[str, Any]: + """Search for movies by title only + + Args: + title: Movie title to search for (partial match, case-insensitive) + + Returns: + List of movies matching the title search + """ + try: + if not title or not title.strip(): + return {"error": "Title is required for search"} + + movies = search_movies_by_title(title.strip()) + + if not movies: + return {"error": f"No movies found with title containing '{title}'"} + + movies_list = [] + for movie_data in movies: + movie = parse_movie_data(movie_data) + movies_list.append({ + "id": movie_data["id"], + "title": movie.title, + "date": movie.date.isoformat(), + "time": movie.time.strftime("%H:%M"), + "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), + "genre": MOVIE_GENRES.get(movie.genre, movie.genre), + "rating": movie.rating, + "seats_remaining": movie.seats_remaining, + "price_per_seat": movie_data.get("price_per_seat"), + "director": movie_data.get("director"), + "is_sold_out": movie.is_sold_out + }) + + return { + "search_title": title, + "total_found": len(movies_list), + "movies": movies_list + } + + except Exception as e: + return {"error": f"Failed to search movies by title: {str(e)}"} \ No newline at end of file diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py index 2caa20f..b6570f6 100644 --- a/src/mcp/cinema-mcp/main.py +++ b/src/mcp/cinema-mcp/main.py @@ -15,6 +15,7 @@ get_current_movies_data, get_movie_details_data, search_movies_data, + search_movies_by_title_data, get_movies_by_date_data ) @@ -49,15 +50,17 @@ def get_movie_details(movie_id: str) -> Dict[str, Any]: @mcp.tool() def search_movies( + title: Optional[str] = None, genre: Optional[str] = None, date: Optional[str] = None, room: Optional[str] = None, available_seats_min: Optional[int] = None, limit: int = 20 ) -> Dict[str, Any]: - """Search for movies with optional filters + """Search for movie presentations with optional filters Args: + title: Filter by movie title (partial match, case-insensitive) genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family) date: Filter by date in YYYY-MM-DD format (e.g., "2025-09-25") room: Filter by cinema room (theater_a, theater_b, theater_c, imax) @@ -65,9 +68,21 @@ def search_movies( limit: Maximum number of results to return (default: 20, max: 100) Returns: - Filtered list of movies matching the search criteria + Filtered list of movie presentations matching the search criteria """ - return search_movies_data(genre, date, room, available_seats_min, limit) + return search_movies_data(title, genre, date, room, available_seats_min, limit) + +@mcp.tool() +def find_movie_by_title(title: str) -> Dict[str, Any]: + """Find movie presentations by title + + Args: + title: Movie title to search for (partial match, case-insensitive) + + Returns: + List of movie presentations matching the title search + """ + return search_movies_by_title_data(title) @mcp.tool() def get_movies_by_date(date: str) -> Dict[str, Any]: diff --git a/src/mcp/cinema-mcp/utils.py b/src/mcp/cinema-mcp/utils.py index fc8a16e..42c3ae7 100644 --- a/src/mcp/cinema-mcp/utils.py +++ b/src/mcp/cinema-mcp/utils.py @@ -41,44 +41,71 @@ def get_movies_by_date(target_date: date) -> List[Dict[str, Any]]: return movies_on_date -def search_movies_by_criteria( +def search_movies_by_title(title: str) -> List[Dict[str, Any]]: + """Search for movie presentations by title (partial match, case-insensitive)""" + matching_movies = [] + search_title = title.lower() + + for movie in MOCK_MOVIE_PRESENTATIONS: + movie_title = movie.get("title", "").lower() + if search_title in movie_title: + matching_movies.append(movie) + + return matching_movies + + +def search_movie_presentations( + title: Optional[str] = None, genre: Optional[str] = None, date: Optional[date] = None, room: Optional[str] = None, available_seats_min: Optional[int] = None, limit: int = 20 ) -> List[Dict[str, Any]]: - """Search movies based on various criteria""" - filtered_movies = [] - - for movie in MOCK_MOVIE_PRESENTATIONS: - # Filter by genre - if genre and movie.get("genre") != genre: - continue + """Search movie presentations based on various criteria""" + filtered_presentations = [] + + for presentation in MOCK_MOVIE_PRESENTATIONS: + # Filter by title (partial match, case-insensitive) + if title: + presentation_title = presentation.get("title", "").lower() + search_title = title.lower() + if search_title not in presentation_title: + continue + + # Filter by genre (case-insensitive) + if genre: + presentation_genre = presentation.get("genre", "").lower() + search_genre = genre.lower() + if presentation_genre != search_genre: + continue # Filter by date - if date and movie.get("date") != date: + if date and presentation.get("date") != date: continue - # Filter by room - if room and movie.get("room") != room: - continue + # Filter by room (case-insensitive) + if room: + presentation_room = presentation.get("room", "").lower() + search_room = room.lower() + if presentation_room != search_room: + continue # Filter by minimum available seats if available_seats_min: - seats_available = movie.get("seats_available", 0) - seats_booked = movie.get("seats_booked", 0) + seats_available = presentation.get("seats_available", 0) + seats_booked = presentation.get("seats_booked", 0) seats_remaining = seats_available - seats_booked if seats_remaining < available_seats_min: continue - filtered_movies.append(movie) + filtered_presentations.append(presentation) # Apply limit - if len(filtered_movies) >= limit: + if len(filtered_presentations) >= limit: break - return filtered_movies + return filtered_presentations def parse_movie_data(movie_data: Dict[str, Any]) -> MovieShowing: From adf9c8b7368c10d4907b98ab9c0d27c49ad0d85b Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:11:44 +0100 Subject: [PATCH 10/25] More changes for movie presentations --- src/mcp/cinema-mcp/cinema_service.py | 32 ++++++++++----- src/mcp/cinema-mcp/main.py | 49 ++++++++-------------- src/mcp/cinema-mcp/utils.py | 61 ++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 44 deletions(-) diff --git a/src/mcp/cinema-mcp/cinema_service.py b/src/mcp/cinema-mcp/cinema_service.py index c449d39..0d21007 100644 --- a/src/mcp/cinema-mcp/cinema_service.py +++ b/src/mcp/cinema-mcp/cinema_service.py @@ -12,7 +12,7 @@ ) from models import MovieShowing, ContactInfo, MovieReservation from utils import ( - get_movie_by_id, search_movie_presentations, get_current_movies, + get_movie_by_id, get_movie_by_natural_id, search_movie_presentations, get_current_movies, get_movies_by_date, search_movies_by_title, format_movie_details, parse_movie_data ) @@ -33,7 +33,6 @@ def get_current_movies_data() -> Dict[str, Any]: for movie_data in current_movies: movie = parse_movie_data(movie_data) movies_list.append({ - "id": movie_data["id"], "title": movie.title, "description": movie.description, "date": movie.date.isoformat(), @@ -61,26 +60,40 @@ def get_current_movies_data() -> Dict[str, Any]: return {"error": f"Failed to get current movies: {str(e)}"} -def get_movie_details_data(movie_id: str) -> Dict[str, Any]: - """Get detailed information about a specific movie +def get_movie_details_data( + title: str, + date: Optional[str] = None, + time: Optional[str] = None, + room: Optional[str] = None +) -> Dict[str, Any]: + """Get detailed information about a specific movie presentation Args: - movie_id: Unique ID of the movie showing + title: Movie title (partial match allowed) + date: Date in YYYY-MM-DD format (optional, helps narrow results) + time: Time in HH:MM or HH:MM AM/PM format (optional, helps narrow results) + room: Room identifier like 'theater_a' or 'Theater A' (optional, helps narrow results) Returns: MovieShowing object as dictionary or error dict """ try: - movie_data = get_movie_by_id(movie_id) + movie_data = get_movie_by_natural_id(title, date, time, room) if not movie_data: - return {"error": f"Movie with ID {movie_id} not found"} + error_msg = f"No movie found with title containing '{title}'" + if date or time or room: + filters = [] + if date: filters.append(f"date: {date}") + if time: filters.append(f"time: {time}") + if room: filters.append(f"room: {room}") + error_msg += f" and filters: {', '.join(filters)}" + return {"error": error_msg} movie = parse_movie_data(movie_data) room_info = CINEMA_ROOMS.get(movie.room, {}) return { "movie": { - "id": movie_data["id"], "title": movie.title, "description": movie.description, "date": movie.date.isoformat(), @@ -159,7 +172,6 @@ def search_movies_data( for movie_data in movies: movie = parse_movie_data(movie_data) movies_list.append({ - "id": movie_data["id"], "title": movie.title, "date": movie.date.isoformat(), "time": movie.time.strftime("%H:%M"), @@ -218,7 +230,6 @@ def get_movies_by_date_data(date: str) -> Dict[str, Any]: for movie_data in movies: movie = parse_movie_data(movie_data) movies_list.append({ - "id": movie_data["id"], "title": movie.title, "time": movie.time.strftime("%H:%M"), "room": CINEMA_ROOMS.get(movie.room, {}).get("name", movie.room), @@ -264,7 +275,6 @@ def search_movies_by_title_data(title: str) -> Dict[str, Any]: for movie_data in movies: movie = parse_movie_data(movie_data) movies_list.append({ - "id": movie_data["id"], "title": movie.title, "date": movie.date.isoformat(), "time": movie.time.strftime("%H:%M"), diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py index b6570f6..d95aa99 100644 --- a/src/mcp/cinema-mcp/main.py +++ b/src/mcp/cinema-mcp/main.py @@ -14,9 +14,7 @@ from cinema_service import ( get_current_movies_data, get_movie_details_data, - search_movies_data, - search_movies_by_title_data, - get_movies_by_date_data + search_movies_data ) mcp = FastMCP("Cinema", port=8009) @@ -37,16 +35,25 @@ def get_current_movies() -> Dict[str, Any]: return get_current_movies_data() @mcp.tool() -def get_movie_details(movie_id: str) -> Dict[str, Any]: +def get_movie_details( + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: """Get detailed information about a specific movie showing Args: - movie_id: Unique ID of the movie showing (e.g., "movie_001") + title: Exact movie title + date: Date in YYYY-MM-DD format (e.g., "2025-09-25") + time: Time in HH:MM format (e.g., "19:30") + room: Room identifier (e.g., "theater_a", "theater_b", "theater_c", "imax") Returns: Detailed movie information including plot, cast, theater details, and availability + Use search_movies first to find the exact title, date, time, and room values needed """ - return get_movie_details_data(movie_id) + return get_movie_details_data(title, date, time, room) @mcp.tool() def search_movies( @@ -72,35 +79,11 @@ def search_movies( """ return search_movies_data(title, genre, date, room, available_seats_min, limit) -@mcp.tool() -def find_movie_by_title(title: str) -> Dict[str, Any]: - """Find movie presentations by title - - Args: - title: Movie title to search for (partial match, case-insensitive) - - Returns: - List of movie presentations matching the title search - """ - return search_movies_by_title_data(title) - -@mcp.tool() -def get_movies_by_date(date: str) -> Dict[str, Any]: - """Get all movies playing on a specific date - - Args: - date: Date in YYYY-MM-DD format (e.g., "2025-09-25") - - Returns: - List of all movie showings scheduled for the specified date, sorted by time - """ - return get_movies_by_date_data(date) - # Resources -@mcp.resource("movie://{movie_id}") -def get_movie_resource(movie_id: str) -> str: +@mcp.resource("movie://{title}/{date}/{time}/{room}") +def get_movie_resource(title: str, date: str, time: str, room: str) -> str: """Get movie details as a formatted resource""" - result = get_movie_details_data(movie_id) + result = get_movie_details_data(title, date, time, room) if "error" in result: return f"Error: {result['error']}" diff --git a/src/mcp/cinema-mcp/utils.py b/src/mcp/cinema-mcp/utils.py index 42c3ae7..d96fa84 100644 --- a/src/mcp/cinema-mcp/utils.py +++ b/src/mcp/cinema-mcp/utils.py @@ -54,6 +54,67 @@ def search_movies_by_title(title: str) -> List[Dict[str, Any]]: return matching_movies +def get_movie_by_natural_id( + title: str, + date: Optional[str] = None, + time: Optional[str] = None, + room: Optional[str] = None +) -> Optional[Dict[str, Any]]: + """Get movie by natural identifiers (title is required, others help narrow down)""" + search_title = title.lower() + + # Parse date if provided + parsed_date = None + if date: + try: + from datetime import datetime + parsed_date = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + return None + + # Parse time if provided + parsed_time = None + if time: + try: + from datetime import datetime + # Try different time formats + for fmt in ["%H:%M", "%I:%M %p", "%I:%M%p"]: + try: + parsed_time = datetime.strptime(time, fmt).time() + break + except ValueError: + continue + except: + pass + + matches = [] + for movie in MOCK_MOVIE_PRESENTATIONS: + # Title must match (partial, case-insensitive) + movie_title = movie.get("title", "").lower() + if search_title not in movie_title: + continue + + # Filter by date if provided + if parsed_date and movie.get("date") != parsed_date: + continue + + # Filter by time if provided + if parsed_time and movie.get("time") != parsed_time: + continue + + # Filter by room if provided (case-insensitive) + if room: + movie_room = movie.get("room", "").lower() + search_room = room.lower() + if movie_room != search_room: + continue + + matches.append(movie) + + # Return the first match, or None if no matches + return matches[0] if matches else None + + def search_movie_presentations( title: Optional[str] = None, genre: Optional[str] = None, From e05664315b42770848b1d504341c4530bb76478e Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:38:16 +0100 Subject: [PATCH 11/25] Reservations and try at notebook --- src/agent/cinema.ipynb | 515 +++++++++++++++++++++++++++ src/mcp/cinema-mcp/cinema_service.py | 248 ++++++++++++- src/mcp/cinema-mcp/main.py | 78 +++- 3 files changed, 837 insertions(+), 4 deletions(-) create mode 100644 src/agent/cinema.ipynb diff --git a/src/agent/cinema.ipynb b/src/agent/cinema.ipynb new file mode 100644 index 0000000..a0f6a69 --- /dev/null +++ b/src/agent/cinema.ipynb @@ -0,0 +1,515 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "a26927c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "python: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Scripts\\python.exe\n", + "uv: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Scripts\\uv.EXE\n" + ] + } + ], + "source": [ + "# Step to ensure that the venv is being used for the project not local copies, should point at .venv in project.\n", + "import sys, shutil\n", + "print(\"python:\", sys.executable)\n", + "print(\"uv:\", shutil.which(\"uv\")) " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "aba67892", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: uv in c:\\users\\andrzejpytel\\source\\hackathon-2025-ap-fork\\.venv\\lib\\site-packages (0.8.22)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[2mUsing Python 3.13.7 environment at: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\u001b[0m\n", + "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 38ms\u001b[0m\u001b[0m\n" + ] + } + ], + "source": [ + "# installs into the current Jupyter kernel environment\n", + "%pip install -U uv \n", + "#! to run shell commands\n", + "!uv pip install -r requirements.txt" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b86c12d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Updated imports with official MCP adapter loaded successfully!\n" + ] + } + ], + "source": [ + "# LangChain + MCP Setup for Cinema Booking (HTTP-based for Jupyter)\n", + "import os\n", + "from dotenv import load_dotenv\n", + "import asyncio\n", + "import json\n", + "import requests\n", + "from typing import Dict, Any, List\n", + "\n", + "# LangChain imports\n", + "from langchain_openai import ChatOpenAI, AzureChatOpenAI\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "from langchain.agents import AgentExecutor, create_tool_calling_agent\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain.memory import ConversationBufferMemory\n", + "\n", + "# Official MCP adapter imports for HTTP transport\n", + "from langchain_mcp_adapters.client import MultiServerMCPClient\n", + "from langchain_mcp_adapters.tools import load_mcp_tools\n", + "\n", + "# Load environment variables\n", + "load_dotenv()\n", + "\n", + "print(\"✅ Updated imports with official MCP adapter loaded successfully!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a60c8345", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔗 Cinema MCP HTTP adapter setup ready!\n" + ] + } + ], + "source": [ + "# MCP Client Setup using Official Adapter with HTTP Transport\n", + "import subprocess\n", + "import time\n", + "\n", + "# Global MCP client for HTTP\n", + "mcp_client = None\n", + "\n", + "async def create_mcp_tools():\n", + " \"\"\"Create MCP tools using the official LangChain MCP adapter with HTTP transport\"\"\"\n", + " global mcp_client\n", + " \n", + " try:\n", + " # Create MultiServerMCPClient with streamable_http transport for cinema\n", + " mcp_client = MultiServerMCPClient({\n", + " \"cinema\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": \"http://localhost:8010\" # Cinema MCP server on port 8010\n", + " }\n", + " })\n", + " \n", + " # Get tools from the MCP server\n", + " tools = await mcp_client.get_tools()\n", + " print(f\"Loaded {len(tools)} MCP tools: {[tool.name for tool in tools]}\")\n", + " return tools\n", + " \n", + " except Exception as e:\n", + " print(f\"Error connecting to Cinema MCP HTTP server: {e}\")\n", + " print(\"Make sure the cinema MCP server is running on port 8010\")\n", + " print(\"Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\")\n", + " return []\n", + "\n", + "print(\"🔗 Cinema MCP HTTP adapter setup ready!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "57e4f778", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🛠️ Ready to load Cinema MCP tools via official adapter!\n" + ] + } + ], + "source": [ + "# Load MCP Tools using Official Adapter\n", + "# The tools will be loaded dynamically when setting up the agent\n", + "# No need to manually create tool wrappers - the adapter handles this automatically\n", + "print(\"🛠️ Ready to load Cinema MCP tools via official adapter!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "501c5c58", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🤖 Cinema agent setup function ready! Run the next cell to initialize.\n" + ] + } + ], + "source": [ + "async def setup_agent():\n", + " \"\"\"Setup LangChain agent with Cinema MCP tools using official adapter\"\"\"\n", + " \n", + " # Initialize LLM for Azure OpenAI\n", + " # can get this from Azure Open AI service -> Azure AI Foundry Portal\n", + " from langchain_openai import AzureChatOpenAI\n", + " \n", + " llm = AzureChatOpenAI(\n", + " deployment_name=os.getenv(\"DEPLOYMENT_NAME\"), # Your Azure deployment name\n", + " api_key=os.getenv(\"AZURE_OPENAI_API_KEY\"),\n", + " azure_endpoint=os.getenv(\"AZURE_OPENAI_ENDPOINT\"), \n", + " api_version=os.getenv(\"AZURE_API_VERSION\"), \n", + " temperature=1\n", + " )\n", + " \n", + " # Load MCP tools using official adapter\n", + " tools = await create_mcp_tools()\n", + " \n", + " if not tools:\n", + " print(\"No MCP tools loaded. Make sure the Cinema MCP server is accessible.\")\n", + " return None\n", + " \n", + " print(f\"Loaded {len(tools)} MCP tools: {[tool.name for tool in tools]}\")\n", + " \n", + " # Create system prompt for cinema assistant\n", + " system_prompt = \"\"\"You are a helpful cinema assistant that can help users discover movies and make reservations.\n", + " \n", + " You have access to cinema MCP tools for movie showings, including:\n", + " - Getting current movies playing with showtimes and availability\n", + " - Searching for movies by title, genre, date, room, and seat availability\n", + " - Getting detailed information about specific movie showings\n", + " - Making movie reservations for customers\n", + " - Viewing customer reservations\n", + " - Canceling reservations\n", + " \n", + " When helping users:\n", + " 1. Use get_current_movies or search_movies to show available options\n", + " 2. Use get_movie_details with exact title, date, time, and room for specific showings\n", + " 3. For reservations, always collect: customer name, email, number of seats wanted\n", + " 4. Use make_reservation with the exact movie details from search results\n", + " 5. Be helpful and provide clear information about showtimes, pricing, and availability\n", + " \n", + " Always be friendly and guide users through the movie booking process step by step.\"\"\"\n", + " \n", + " # Create prompt template\n", + " prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"chat_history\"),\n", + " (\"human\", \"{input}\"),\n", + " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", + " ])\n", + " \n", + " # Create agent\n", + " agent = create_tool_calling_agent(llm, tools, prompt)\n", + "\n", + " # memory\n", + " memory = ConversationBufferMemory(memory_key=\"chat_history\", return_messages=True)\n", + "\n", + " # Create agent executor with tool logging callback and verbose output\n", + " agent_executor = AgentExecutor(agent=agent, tools=tools, memory=memory, verbose=True)\n", + " \n", + " return agent_executor\n", + "\n", + "# Initialize the agent (now async)\n", + "agent_executor = None\n", + "print(\"🤖 Cinema agent setup function ready! Run the next cell to initialize.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "688da57b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initializing cinema agent with MCP tools...\n", + "Error connecting to Cinema MCP HTTP server: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Make sure the cinema MCP server is running on port 8010\n", + "Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\n", + "No MCP tools loaded. Make sure the Cinema MCP server is accessible.\n", + "Failed to initialize agent. Check Cinema MCP server connection.\n", + "Error connecting to Cinema MCP HTTP server: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Make sure the cinema MCP server is running on port 8010\n", + "Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\n", + "No MCP tools loaded. Make sure the Cinema MCP server is accessible.\n", + "Failed to initialize agent. Check Cinema MCP server connection.\n" + ] + } + ], + "source": [ + "# Initialize the agent with Cinema MCP tools\n", + "async def initialize_agent():\n", + " \"\"\"Initialize the agent with Cinema MCP tools\"\"\"\n", + " global agent_executor\n", + " print(\"Initializing cinema agent with MCP tools...\")\n", + " agent_executor = await setup_agent()\n", + " if agent_executor:\n", + " print(\"LangChain cinema agent with MCP tools ready!\")\n", + " else:\n", + " print(\"Failed to initialize agent. Check Cinema MCP server connection.\")\n", + "\n", + "# Run the initialization\n", + "await initialize_agent()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "dc7277da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "💬 Cinema user input handler ready!\n" + ] + } + ], + "source": [ + "# User Input Handler + logged agent steps\n", + "async def process_user_input(user_input: str) -> str:\n", + " \"\"\"Process user input and return LLM response using Cinema MCP tools\"\"\"\n", + " if not agent_executor:\n", + " return \"Agent not initialized. Please run the initialization cell first.\"\n", + " \n", + " try:\n", + " # Use the agent to process the input and get intermediate steps\n", + " result = await agent_executor.ainvoke({\"input\": user_input})\n", + " output = result.get(\"output\") or result.get(\"final_output\") or \"\"\n", + "\n", + " # Print intermediate steps if present\n", + " steps = result.get(\"intermediate_steps\") or []\n", + " for step in steps:\n", + " action = None\n", + " observation = None\n", + " if isinstance(step, tuple) and len(step) == 2:\n", + " action, observation = step\n", + " elif isinstance(step, dict) and \"action\" in step:\n", + " action = step.get(\"action\")\n", + " observation = step.get(\"observation\")\n", + " else:\n", + " continue\n", + "\n", + " tool_name = getattr(action, \"tool\", getattr(action, \"tool_name\", \"unknown\"))\n", + " tool_args = getattr(action, \"tool_input\", getattr(action, \"input\", None))\n", + " print(f\"\\n--- Tool: {tool_name}\")\n", + " print(f\"args: {tool_args}\")\n", + " if observation is not None:\n", + " print(f\"result: {observation}\")\n", + " print(\"---\\n\")\n", + "\n", + " return output\n", + " except Exception as e:\n", + " return f\"Error processing request: {str(e)}\"\n", + "\n", + "# Interactive function for easy testing\n", + "async def ask_assistant(question: str):\n", + " \"\"\"Easy-to-use function for asking the cinema assistant\"\"\"\n", + " print(f\"🎬 User: {question}\")\n", + " print(\"🤖 Assistant:\")\n", + " \n", + " response = await process_user_input(question)\n", + " print(response)\n", + " return response\n", + "\n", + "print(\"💬 Cinema user input handler ready!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3c70994e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error connecting to Cinema MCP HTTP server: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Make sure the cinema MCP server is running on port 8010\n", + "Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\n", + "Failed to connect to Cinema MCP HTTP server\n", + "Make sure to start the cinema MCP server first:\n", + "cd src/mcp/cinema-mcp && uv run mcp dev main.py\n" + ] + } + ], + "source": [ + "# Test Cinema MCP server connectivity and tools\n", + "async def test_mcp_connection():\n", + " \"\"\"Test Cinema MCP server connection and list available tools\"\"\"\n", + " tools = await create_mcp_tools()\n", + " if tools:\n", + " print(f\"Cinema MCP HTTP server connected successfully!\")\n", + " print(f\"Available tools: {[tool.name for tool in tools]}\")\n", + " for tool in tools:\n", + " print(f\" - {tool.name}: {tool.description}\")\n", + " else:\n", + " print(\"Failed to connect to Cinema MCP HTTP server\")\n", + " print(\"Make sure to start the cinema MCP server first:\")\n", + " print(\"cd src/mcp/cinema-mcp && uv run mcp dev main.py\")\n", + "\n", + "# Test Cinema MCP HTTP connection\n", + "await test_mcp_connection()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1551f227", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🎬 User: What movies are currently playing?\n", + "🤖 Assistant:\n", + "Agent not initialized. Please run the initialization cell first.\n", + "\n", + "==================================================\n", + "\n" + ] + } + ], + "source": [ + "# 🚀 EXAMPLE USAGE - Run this cell after setting up your API key!\n", + "\n", + "# Simple question about current movies\n", + "await ask_assistant(\"What movies are currently playing?\")\n", + "print(\"\\n\" + \"=\"*50 + \"\\n\")\n", + "\n", + "# Example 2: Search for specific type of movie\n", + "# await ask_assistant(\"I'm looking for action movies playing tomorrow. What do you have?\")\n", + "# print(\"\\n\" + \"=\"*50 + \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9295d748", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🎬 Welcome to MovieMagic Cinema Assistant!\n", + "I can help you find movies, check showtimes, and make reservations.\n", + "Type 'exit' to quit. Press Enter on an empty line to skip.\n", + "\n" + ] + } + ], + "source": [ + "# Interactive chat loop — keep asking questions until you exit\n", + "\n", + "# Try these example questions:\n", + "# - \"What movies are playing today?\"\n", + "# - \"Show me action movies\"\n", + "# - \"I want to see Avatar tomorrow evening\"\n", + "# - \"Get me details for Avatar on 2025-09-25 at 19:30 in theater_a\"\n", + "# - \"Book 2 seats for Avatar on 2025-09-25 at 19:30 in theater_a for John Doe, john@email.com\"\n", + "# - \"Show my reservations for john@email.com\"\n", + "\n", + "async def chat_loop():\n", + " print(\"🎬 Welcome to MovieMagic Cinema Assistant!\")\n", + " print(\"I can help you find movies, check showtimes, and make reservations.\")\n", + " print(\"Type 'exit' to quit. Press Enter on an empty line to skip.\\n\")\n", + " \n", + " while True:\n", + " try:\n", + " question = input(\"You: \").strip()\n", + " except (EOFError, KeyboardInterrupt):\n", + " print(\"\\nExiting.\")\n", + " break\n", + " if not question:\n", + " continue\n", + " if question.lower() in (\"exit\", \"quit\", \"q\"):\n", + " print(\"🎬 Thanks for using MovieMagic Cinema! Goodbye!\")\n", + " break\n", + " await ask_assistant(question)\n", + "\n", + "# Start the cinema chat loop\n", + "await chat_loop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e70dad2d", + "metadata": {}, + "outputs": [], + "source": [ + "# Cleanup function for HTTP MCP client\n", + "async def cleanup_mcp():\n", + " \"\"\"Cleanup MCP client and server resources\"\"\"\n", + " global mcp_client\n", + " if mcp_client:\n", + " try:\n", + " await mcp_client.close()\n", + " print(\"Cinema MCP client closed\")\n", + " except Exception as e:\n", + " print(f\"Warning: Error closing Cinema MCP client: {e}\")\n", + " mcp_client = None\n", + "\n", + "print(\"🧹 Cleanup function ready!\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/mcp/cinema-mcp/cinema_service.py b/src/mcp/cinema-mcp/cinema_service.py index 0d21007..d4b6844 100644 --- a/src/mcp/cinema-mcp/cinema_service.py +++ b/src/mcp/cinema-mcp/cinema_service.py @@ -294,4 +294,250 @@ def search_movies_by_title_data(title: str) -> Dict[str, Any]: } except Exception as e: - return {"error": f"Failed to search movies by title: {str(e)}"} \ No newline at end of file + return {"error": f"Failed to search movies by title: {str(e)}"} + + +# In-memory storage for reservations (in production, this would be a database) +RESERVATIONS: List[MovieReservation] = [] + +def create_reservation_data( + title: str, + date: str, + time: str, + room: str, + seats_count: int, + customer_name: str, + customer_email: str, + customer_phone: Optional[str] = None, + special_requests: Optional[str] = None +) -> Dict[str, Any]: + """Create a new movie reservation + + Args: + title: Exact movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + seats_count: Number of seats to reserve + customer_name: Customer's full name + customer_email: Customer's email address + customer_phone: Customer's phone number (optional) + special_requests: Any special requests (optional) + + Returns: + Reservation confirmation details or error dict + """ + try: + # Find the movie showing + movie_data = get_movie_by_natural_id(title, date, time, room) + if not movie_data: + return {"error": f"No movie found: '{title}' on {date} at {time} in {room}"} + + movie = parse_movie_data(movie_data) + + # Check seat availability + if seats_count <= 0: + return {"error": "Number of seats must be greater than 0"} + + if seats_count > movie.seats_remaining: + return {"error": f"Not enough seats available. Only {movie.seats_remaining} seats remaining"} + + # Validate contact info + if not customer_name or not customer_name.strip(): + return {"error": "Customer name is required"} + + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + # Basic email validation + if "@" not in customer_email or "." not in customer_email: + return {"error": "Please provide a valid email address"} + + # Create contact info + contact_info = ContactInfo( + name=customer_name.strip(), + email=customer_email.strip().lower(), + phone=customer_phone.strip() if customer_phone else None + ) + + # Parse date and time + try: + reservation_date = datetime.strptime(date, "%Y-%m-%d").date() + reservation_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Create reservation + reservation = MovieReservation( + movie_title=title, + movie_date=reservation_date, + movie_time=reservation_time, + room=room, + seats_reserved=seats_count, + contact_info=contact_info, + reservation_datetime=datetime.now(), + status="confirmed", + special_requests=special_requests.strip() if special_requests else None + ) + + # Add to storage + RESERVATIONS.append(reservation) + + # Update movie booking count (simulate booking the seats) + movie_data["seats_booked"] = movie_data.get("seats_booked", 0) + seats_count + + # Generate confirmation + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + genre_name = MOVIE_GENRES.get(movie.genre, movie.genre) + + return { + "confirmation": { + "reservation_id": len(RESERVATIONS), # Simple ID based on list length + "status": "confirmed", + "reservation_datetime": reservation.reservation_datetime.isoformat() + }, + "movie_details": { + "title": title, + "date": date, + "time": time, + "room": room_name, + "genre": genre_name, + "duration_minutes": movie.duration_minutes + }, + "booking_details": { + "seats_reserved": seats_count, + "customer_name": customer_name, + "customer_email": customer_email, + "customer_phone": customer_phone, + "special_requests": special_requests + }, + "pricing": { + "price_per_seat": movie_data.get("price_per_seat", 0), + "total_price": movie_data.get("price_per_seat", 0) * seats_count + } + } + + except Exception as e: + return {"error": f"Failed to create reservation: {str(e)}"} + + +def get_customer_reservations_data(customer_email: str) -> Dict[str, Any]: + """Get all reservations for a customer by email + + Args: + customer_email: Customer's email address + + Returns: + List of customer's reservations or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + customer_reservations = [ + res for res in RESERVATIONS + if res.customer_email.lower() == email + ] + + if not customer_reservations: + return {"error": f"No reservations found for {customer_email}"} + + reservations_list = [] + for i, reservation in enumerate(customer_reservations, 1): + room_name = CINEMA_ROOMS.get(reservation.room, {}).get("name", reservation.room) + + reservations_list.append({ + "reservation_number": i, + "movie_title": reservation.movie_title, + "date": reservation.movie_date.isoformat(), + "time": reservation.movie_time.strftime("%H:%M"), + "room": room_name, + "seats_reserved": reservation.seats_reserved, + "status": reservation.status, + "reservation_made": reservation.reservation_datetime.isoformat(), + "special_requests": reservation.special_requests + }) + + return { + "customer_email": customer_email, + "total_reservations": len(reservations_list), + "reservations": reservations_list + } + + except Exception as e: + return {"error": f"Failed to get customer reservations: {str(e)}"} + + +def cancel_reservation_data( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Cancel a reservation + + Args: + customer_email: Customer's email address + title: Movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Cancellation confirmation or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + + # Parse date and time for comparison + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + target_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Find the reservation + reservation_to_cancel = None + for reservation in RESERVATIONS: + if (reservation.customer_email.lower() == email and + reservation.movie_title.lower() == title.lower() and + reservation.movie_date == target_date and + reservation.movie_time == target_time and + reservation.room.lower() == room.lower() and + reservation.status == "confirmed"): + reservation_to_cancel = reservation + break + + if not reservation_to_cancel: + return {"error": f"No confirmed reservation found for {customer_email} for '{title}' on {date} at {time} in {room}"} + + # Cancel the reservation + reservation_to_cancel.status = "cancelled" + + # Free up the seats (simulate returning seats to availability) + movie_data = get_movie_by_natural_id(title, date, time, room) + if movie_data: + movie_data["seats_booked"] = max(0, movie_data.get("seats_booked", 0) - reservation_to_cancel.seats_reserved) + + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + + return { + "cancellation_confirmed": True, + "cancelled_reservation": { + "movie_title": title, + "date": date, + "time": time, + "room": room_name, + "seats_freed": reservation_to_cancel.seats_reserved, + "customer_email": customer_email + }, + "message": f"Reservation cancelled successfully. {reservation_to_cancel.seats_reserved} seats have been freed up." + } + + except Exception as e: + return {"error": f"Failed to cancel reservation: {str(e)}"} \ No newline at end of file diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py index d95aa99..41f316e 100644 --- a/src/mcp/cinema-mcp/main.py +++ b/src/mcp/cinema-mcp/main.py @@ -14,10 +14,13 @@ from cinema_service import ( get_current_movies_data, get_movie_details_data, - search_movies_data + search_movies_data, + create_reservation_data, + get_customer_reservations_data, + cancel_reservation_data ) -mcp = FastMCP("Cinema", port=8009) +mcp = FastMCP("Cinema", port=8010) # Tools @mcp.tool() @@ -79,6 +82,75 @@ def search_movies( """ return search_movies_data(title, genre, date, room, available_seats_min, limit) +@mcp.tool() +def make_reservation( + title: str, + date: str, + time: str, + room: str, + seats_count: int, + customer_name: str, + customer_email: str, + customer_phone: Optional[str] = None, + special_requests: Optional[str] = None +) -> Dict[str, Any]: + """Create a movie reservation for a specific showing + + Args: + title: Exact movie title (use get_movie_details or search_movies to find exact title) + date: Date in YYYY-MM-DD format (e.g., "2025-09-25") + time: Time in HH:MM format (e.g., "19:30") + room: Room identifier (e.g., "theater_a", "theater_b", "theater_c", "imax") + seats_count: Number of seats to reserve (must be > 0) + customer_name: Customer's full name + customer_email: Customer's email address + customer_phone: Customer's phone number (optional) + special_requests: Any special requests or accessibility needs (optional) + + Returns: + Reservation confirmation with booking details and pricing information + Use the exact title, date, time, and room values from search_movies or get_current_movies results + """ + return create_reservation_data( + title, date, time, room, seats_count, + customer_name, customer_email, customer_phone, special_requests + ) + +@mcp.tool() +def get_my_reservations(customer_email: str) -> Dict[str, Any]: + """Get all reservations for a customer + + Args: + customer_email: Customer's email address used for reservations + + Returns: + List of all reservations made by the customer + """ + return get_customer_reservations_data(customer_email) + +@mcp.tool() +def cancel_reservation( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Cancel a movie reservation + + Args: + customer_email: Customer's email address + title: Exact movie title of the reservation to cancel + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Cancellation confirmation and details about freed seats + Use get_my_reservations first to find the exact details of reservations to cancel + """ + return cancel_reservation_data(customer_email, title, date, time, room) + # Resources @mcp.resource("movie://{title}/{date}/{time}/{room}") def get_movie_resource(title: str, date: str, time: str, room: str) -> str: @@ -140,4 +212,4 @@ def get_current_movies_resource() -> str: return output if __name__ == "__main__": - mcp.run() \ No newline at end of file + mcp.run(transport="streamable-http") \ No newline at end of file From ba1a1f839be0718dafaf4d253d5b1df3dc040b8d Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 14:03:33 +0100 Subject: [PATCH 12/25] Updating all references to movies --- src/mcp/movie-reviews-mcp/__init__.py | 47 ++-- src/mcp/movie-reviews-mcp/config.py | 273 +++++++++++++++++++ src/mcp/movie-reviews-mcp/main.py | 165 +++++------- src/mcp/movie-reviews-mcp/models.py | 6 + src/mcp/movie-reviews-mcp/movie_service.py | 288 +++++---------------- src/mcp/movie-reviews-mcp/pyproject.toml | 4 +- src/mcp/movie-reviews-mcp/readme.md | 128 +++------ src/mcp/movie-reviews-mcp/utils.py | 130 +++------- 8 files changed, 489 insertions(+), 552 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/__init__.py b/src/mcp/movie-reviews-mcp/__init__.py index 0ed8e76..d6876ce 100644 --- a/src/mcp/movie-reviews-mcp/__init__.py +++ b/src/mcp/movie-reviews-mcp/__init__.py @@ -1,21 +1,19 @@ """ -Tourist Attractions MCP Package - Discover and book attractions worldwide. +Movie Reviews MCP Package - Discover list of movies and reviews """ -from attractions_service import ( - get_attraction_details_data, - search_attractions_data, - get_random_attraction_data, - get_world_wonders_data, - book_attraction_data, - get_attraction_categories_data +from movie_service import ( + get_movie_details_data, + search_movies_data, + get_random_famous_movie_data, + get_movie_reviews_data, + format_movie_resource ) from models import ( - Attraction, AttractionDetails, BookingRequest, BookingResponse, - AttractionsList, SearchFilters, Location, Coordinates + Movie, MovieReview, MovieList ) from utils import ( - get_attraction_by_id, search_attractions, parse_attraction_data, + get_movie_by_id, search_movies, parse_movie_data, format_attraction_name, get_category_display_name, generate_booking_id, validate_visit_date, validate_email, format_attraction_details ) @@ -23,28 +21,23 @@ __version__ = "1.0.0" __all__ = [ # Service functions - "get_attraction_details_data", - "search_attractions_data", - "get_random_attraction_data", - "get_world_wonders_data", - "book_attraction_data", - "get_attraction_categories_data", + "parse_movie_data", + "search_movies_data", + "get_random_famous_movie_data", + "get_movie_details_data", + "get_movie_reviews_data", + "format_movie_resource", # Models - "Attraction", - "AttractionDetails", - "BookingRequest", - "BookingResponse", - "AttractionsList", - "SearchFilters", - "Location", - "Coordinates", + "Movie", + "MovieList", + "MovieReview", # Utilities - "get_attraction_by_id", + "get_movie_by_id", "search_attractions", "parse_attraction_data", "format_attraction_name", "get_category_display_name", - "generate_booking_id", + "generate_booking_id", "validate_visit_date", "validate_email", "format_attraction_details" diff --git a/src/mcp/movie-reviews-mcp/config.py b/src/mcp/movie-reviews-mcp/config.py index 0742b6b..0aa58a6 100644 --- a/src/mcp/movie-reviews-mcp/config.py +++ b/src/mcp/movie-reviews-mcp/config.py @@ -182,4 +182,277 @@ "durationMins": 106, "genre": "drama" } +] + +MOCK_REVIEWS = [ + { + "movie_id": 1, + "reviewer": "Alice", + "rating": 5, + "comment": "A mind-bending masterpiece with stunning visuals and a gripping plot." + }, + { + "movie_id": 1, + "reviewer": "Bob", + "rating": 4, + "comment": "Complex and thought-provoking, but a bit hard to follow at times." + }, + { + "movie_id": 2, + "reviewer": "Charlie", + "rating": 5, + "comment": "An epic tale of family and power. A must-watch classic." + }, + { + "movie_id": 2, + "reviewer": "Diana", + "rating": 5, + "comment": "Brilliant performances and an unforgettable story." + }, + { + "movie_id": 3, + "reviewer": "Eve", + "rating": 5, + "comment": "Heath Ledger's Joker is iconic. A thrilling ride from start to finish." + }, + { + "movie_id": 3, + "reviewer": "Frank", + "rating": 4, + "comment": "Great action and depth, though a bit dark for my taste." + }, + { + "movie_id": 4, + "reviewer": "Grace", + "rating": 5, + "comment": "A wild, stylish film with unforgettable dialogue." + }, + { + "movie_id": 4, + "reviewer": "Henry", + "rating": 4, + "comment": "Unique storytelling and great cast." + }, + { + "movie_id": 5, + "reviewer": "Ivy", + "rating": 5, + "comment": "Heartwarming and inspirational. Tom Hanks is fantastic." + }, + { + "movie_id": 5, + "reviewer": "Jack", + "rating": 4, + "comment": "A touching story with memorable moments." + }, + { + "movie_id": 6, + "reviewer": "Karen", + "rating": 5, + "comment": "Visually stunning and emotionally powerful." + }, + { + "movie_id": 6, + "reviewer": "Leo", + "rating": 4, + "comment": "Ambitious sci-fi with a moving story." + }, + { + "movie_id": 7, + "reviewer": "Mona", + "rating": 5, + "comment": "A brilliant social satire with suspenseful twists." + }, + { + "movie_id": 7, + "reviewer": "Nate", + "rating": 4, + "comment": "Darkly funny and deeply unsettling." + }, + { + "movie_id": 8, + "reviewer": "Olivia", + "rating": 5, + "comment": "A powerful and emotional historical drama." + }, + { + "movie_id": 8, + "reviewer": "Paul", + "rating": 5, + "comment": "Heart-wrenching and beautifully acted." + }, + { + "movie_id": 9, + "reviewer": "Quinn", + "rating": 5, + "comment": "A moving story of hope and friendship." + }, + { + "movie_id": 9, + "reviewer": "Rita", + "rating": 5, + "comment": "Timeless and uplifting. A true classic." + }, + { + "movie_id": 10, + "reviewer": "Sam", + "rating": 5, + "comment": "Magical animation with a rich, imaginative world." + }, + { + "movie_id": 10, + "reviewer": "Tina", + "rating": 4, + "comment": "Beautiful visuals and a touching story." + }, + { + "movie_id": 11, + "reviewer": "Uma", + "rating": 5, + "comment": "Epic battles and a compelling hero." + }, + { + "movie_id": 11, + "reviewer": "Victor", + "rating": 4, + "comment": "Intense action and strong performances." + }, + { + "movie_id": 12, + "reviewer": "Wendy", + "rating": 5, + "comment": "Mind-blowing concept and great action." + }, + { + "movie_id": 12, + "reviewer": "Xander", + "rating": 4, + "comment": "Innovative and entertaining sci-fi." + }, + { + "movie_id": 13, + "reviewer": "Yara", + "rating": 5, + "comment": "Dark, clever, and unforgettable." + }, + { + "movie_id": 13, + "reviewer": "Zane", + "rating": 4, + "comment": "Unique story with strong performances." + }, + { + "movie_id": 14, + "reviewer": "Abby", + "rating": 5, + "comment": "Epic fantasy with breathtaking visuals." + }, + { + "movie_id": 14, + "reviewer": "Ben", + "rating": 5, + "comment": "A fitting end to a legendary trilogy." + }, + { + "movie_id": 15, + "reviewer": "Cara", + "rating": 5, + "comment": "A childhood favorite with memorable songs." + }, + { + "movie_id": 15, + "reviewer": "Dan", + "rating": 4, + "comment": "Beautiful animation and a touching story." + }, + { + "movie_id": 16, + "reviewer": "Ella", + "rating": 5, + "comment": "Gripping crime drama with stellar acting." + }, + { + "movie_id": 16, + "reviewer": "Finn", + "rating": 4, + "comment": "Intense and well-crafted mob story." + }, + { + "movie_id": 17, + "reviewer": "Gina", + "rating": 5, + "comment": "Chilling and suspenseful thriller." + }, + { + "movie_id": 17, + "reviewer": "Hank", + "rating": 4, + "comment": "Great performances and a gripping plot." + }, + { + "movie_id": 18, + "reviewer": "Iris", + "rating": 5, + "comment": "A powerful and realistic war film." + }, + { + "movie_id": 18, + "reviewer": "Jake", + "rating": 4, + "comment": "Intense and emotional storytelling." + }, + { + "movie_id": 19, + "reviewer": "Kara", + "rating": 5, + "comment": "A fascinating tale of rivalry and obsession." + }, + { + "movie_id": 19, + "reviewer": "Liam", + "rating": 4, + "comment": "Intriguing plot with excellent twists." + }, + { + "movie_id": 20, + "reviewer": "Mia", + "rating": 5, + "comment": "Electrifying performances and a compelling story." + }, + { + "movie_id": 20, + "reviewer": "Noah", + "rating": 4, + "comment": "Intense and inspiring musical drama." + }, + { + "movie_id": 1, + "reviewer": "Oscar", + "rating": 2, + "comment": "Too confusing and dragged on for too long." + }, + { + "movie_id": 5, + "reviewer": "Pam", + "rating": 2, + "comment": "Overrated and slow, didn't connect with the story." + }, + { + "movie_id": 7, + "reviewer": "Quentin", + "rating": 1, + "comment": "Found it boring and hard to follow." + }, + { + "movie_id": 12, + "reviewer": "Ralph", + "rating": 2, + "comment": "Not as groundbreaking as people say, felt dated." + }, + { + "movie_id": 15, + "reviewer": "Sophie", + "rating": 1, + "comment": "Didn't enjoy the animation style or the music." + } ] \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py index 5bd10d3..1b20710 100644 --- a/src/mcp/movie-reviews-mcp/main.py +++ b/src/mcp/movie-reviews-mcp/main.py @@ -11,153 +11,108 @@ from typing import Dict, Any, Optional from mcp.server.fastmcp import FastMCP -from attractions_service import ( - get_attraction_details_data, - search_attractions_data, - get_random_attraction_data, - get_world_wonders_data, - book_attraction_data, - get_attraction_categories_data, - format_attraction_resource, - get_booking_summary_prompt, +from movie_service import ( + get_movie_genres_data, + get_random_movie_data, + parse_movie_data, + search_movies_data, + get_random_famous_movie_data, + get_movie_details_data, + get_movie_reviews_data, + format_movie_resource, format_search_results ) -mcp = FastMCP("Attractions", port=8008) +mcp = FastMCP("MovieReviews", port=8011) # tools @mcp.tool() -def get_attraction_details(attraction_id: int) -> Dict[str, Any]: - """Get detailed information about a specific tourist attraction - +def get_movie_details(movie_id: int) -> Dict[str, Any]: + """Get detailed information about a specific movie + Args: - attraction_id: Unique ID of the attraction - + movie_id: Unique ID of the movie + Returns: - AttractionDetails object as dictionary with attraction info, facilities, and visiting tips + MovieDetails object as dictionary with movie info, reviews, and related movies """ - return get_attraction_details_data(attraction_id) + return get_movie_details_data(movie_id) @mcp.tool() -def search_attractions( - location: Optional[str] = None, - category: Optional[str] = None, +def search_movies( + genre: Optional[str] = None, limit: int = 20 ) -> Dict[str, Any]: - """Search for tourist attractions with optional filters - + """Search for movies with optional filters + Args: - location: Location to search in (e.g., "Paris", "India", "Italy") - category: Category filter - "historical", "natural", "cultural", "religious", "modern", "museums", "parks", "beaches", "mountains", "architecture", "entertainment", "adventure" + genre: Genre filter - "action", "comedy", "drama", etc. limit: Maximum number of results (1-100, default: 20) - - Returns: - AttractionsList object as dictionary with matching attractions - """ - return search_attractions_data(location, category, limit) -@mcp.tool() -def get_random_attraction(region: str = "famous") -> Dict[str, Any]: - """Get a random tourist attraction for inspiration - - Args: - region: Region type - "famous" for world famous attractions, "india" for Indian attractions - Returns: - Attraction object as dictionary with random attraction details + MoviesList object as dictionary with matching movies """ - return get_random_attraction_data(region) + return search_movies_data(genre, limit) @mcp.tool() -def book_attraction( - attraction_id: int, - visitor_name: str, - email: str, - visit_date: str, - num_visitors: int = 1, - phone: Optional[str] = None, - special_requirements: Optional[str] = None -) -> Dict[str, Any]: - """Book a visit to a tourist attraction - +def get_random_movie() -> Dict[str, Any]: + """Get a random movie for inspiration + Args: - attraction_id: ID of the attraction to book - visitor_name: Name of the primary visitor - email: Email address for booking confirmation - visit_date: Visit date in YYYY-MM-DD format - num_visitors: Number of visitors (1-50, default: 1) - phone: Optional phone number - special_requirements: Optional special requirements or requests - + genre: Genre type - "famous" for world famous movies, "india" for Indian movies + Returns: - BookingResponse object as dictionary with booking confirmation details + Movie object as dictionary with random movie details """ - return book_attraction_data( - attraction_id, visitor_name, email, visit_date, - num_visitors, phone, special_requirements - ) + return get_random_movie_data() + + @mcp.tool() -def search_and_format_attractions( - location: Optional[str] = None, - category: Optional[str] = None, +def search_and_format_movies( + genre: Optional[str] = None, limit: int = 10 ) -> str: - """Search for attractions and return formatted results for easy reading - + """Search for movies and return formatted results for easy reading + Args: - location: Location to search in (e.g., "Paris", "India", "Italy") - category: Category filter (e.g., "historical", "natural", "cultural") + genre: Genre filter (e.g., "action", "comedy", "drama") limit: Maximum number of results (1-20, default: 10) - + Returns: - Formatted string with attraction search results + Formatted string with movie search results """ if limit > 20: limit = 20 - - search_data = search_attractions_data(location, category, limit) - return format_search_results(search_data) - -# resources -@mcp.resource("attraction://{attraction_id}") -def get_attraction_resource(attraction_id: int) -> str: - """Get attraction information as a formatted resource""" - return format_attraction_resource(attraction_id) -@mcp.resource("attractions://search/{location}") -def get_attractions_by_location_resource(location: str) -> str: - """Get attractions for a location as a formatted resource""" - search_data = search_attractions_data(location=location, limit=10) + search_data = search_movies_data(genre, limit) return format_search_results(search_data) -@mcp.resource("attractions://category/{category}") -def get_attractions_by_category_resource(category: str) -> str: - """Get attractions by category as a formatted resource""" - search_data = search_attractions_data(category=category, limit=10) +# resources +@mcp.resource("movie://{movie_id}") +def get_movie_resource(movie_id: int) -> str: + """Get movie information as a formatted resource""" + return format_movie_resource(movie_id) + +@mcp.resource("movies://genre/{genre}") +def get_movies_by_genre_resource(genre: str) -> str: + """Get movies by genre as a formatted resource""" + search_data = search_movies_data(genre=genre, limit=10) return format_search_results(search_data) -@mcp.resource("attractions://category") -def get_attractions() -> str: - """Get 10 attractions by formatted resource""" - search_data = search_attractions_data(limit=10) +@mcp.resource("movies://genre") +def get_movies() -> str: + """Get 10 movies by formatted resource""" + search_data = search_movies_data(limit=10) return format_search_results(search_data) -@mcp.resource("attractions://categories") -def get_attraction_categories_resource() -> str: - """Get list of available attraction categories as a formatted resource""" - return get_attraction_categories_data() +@mcp.resource("movies://genres") +def get_movie_genres_resource() -> str: + """Get list of available movie genres as a formatted resource""" + return get_movie_genres_data() -@mcp.resource("attractions://wonders") -def get_world_wonders_resource() -> str: - """Get list of the Wonders of the World attractions as a formatted resource""" - return get_world_wonders_data() # prompts -@mcp.prompt() -def attraction_booking_prompt(location: str, category: Optional[str] = None) -> str: - """Generate a prompt for attraction booking assistance""" - return get_booking_summary_prompt(location, category) @mcp.prompt() def travel_planning_prompt(location: str, days: int = 3) -> str: @@ -178,7 +133,7 @@ def attraction_comparison_prompt(attraction_ids: str) -> str: """Generate a prompt for comparing multiple attractions""" return f"""Please provide a detailed comparison of these attractions (IDs: {attraction_ids}), including: 1. Unique features and highlights of each -2. Best times to visit and crowd levels +2. Best times to visit and crowd levels 3. Entry requirements and booking procedures 4. Approximate visit duration 5. Nearby attractions and activities diff --git a/src/mcp/movie-reviews-mcp/models.py b/src/mcp/movie-reviews-mcp/models.py index babbd31..0df5baa 100644 --- a/src/mcp/movie-reviews-mcp/models.py +++ b/src/mcp/movie-reviews-mcp/models.py @@ -25,3 +25,9 @@ class Movie: year: int genres: List[str] reviews: List[MovieReview] + +@dataclass +class MoviesList: + genre: str + total_count: int = 0 + movies: List[Movie] = None diff --git a/src/mcp/movie-reviews-mcp/movie_service.py b/src/mcp/movie-reviews-mcp/movie_service.py index 44aff0f..554be28 100644 --- a/src/mcp/movie-reviews-mcp/movie_service.py +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -6,63 +6,30 @@ from dataclasses import asdict from datetime import datetime -from config import DEFAULT_SEARCH_LIMIT, ATTRACTION_CATEGORIES +from config import DEFAULT_SEARCH_LIMIT, MOVIE_GENRES from models import ( - Movie, MovieReview + Movie, MovieReview, MoviesList ) from utils import ( - get_attraction_by_id, search_attractions, get_random_famous_attraction, - get_random_india_attraction, get_wonders_of_world, parse_attraction_data, - format_attraction_name, get_category_display_name, generate_booking_id, - generate_confirmation_code, validate_visit_date, validate_email, - calculate_estimated_cost, format_attraction_details + get_genre_display_name, + parse_movie_data, + search_movies, + get_random_famous_movie ) -def get_attraction_details_data(attraction_id: int) -> Dict[str, Any]: - """Get detailed information about a specific attraction - - Args: - attraction_id: Unique ID of the attraction - - Returns: - AttractionDetails object as dictionary or error dict - """ - try: - data = get_attraction_by_id(attraction_id) - if not data: - return {"error": f"Attraction with ID {attraction_id} not found"} - - attraction = parse_attraction_data(data) - - attraction_details = AttractionDetails( - attraction=attraction, - reviews_count=data.get("reviews_count"), - facilities=data.get("facilities", []), - best_time_to_visit=data.get("best_time_to_visit"), - duration=data.get("duration") - ) - - return asdict(attraction_details) - - except Exception as e: - return {"error": f"Failed to get attraction details: {str(e)}"} - - -def search_attractions_data( - location: str = None, - category: str = None, +def search_movies_data( + genre: str = None, limit: int = DEFAULT_SEARCH_LIMIT ) -> Dict[str, Any]: - """Search for attractions with filters + """Search for movies with filters Args: - location: Location to search in (e.g., "Paris", "India", "Italy") - category: Category of attractions (e.g., "historical", "natural", "cultural") + genre: Genre of movies (e.g., "action", "comedy", "drama") limit: Maximum number of results (default: 20, max: 100) Returns: - AttractionsList object as dictionary or error dict + MoviesList object as dictionary or error dict """ try: if limit > 100: @@ -70,236 +37,97 @@ def search_attractions_data( elif limit < 1: limit = 1 - data = search_attractions(location, category, limit) + data = search_movies( genre, limit) if not data: - return {"error": "No attractions found matching the criteria"} - - attractions = [] - if "attractions" in data: - for item in data["attractions"]: - attraction = parse_attraction_data(item) - attractions.append(attraction) - - attractions_list = AttractionsList( - category=get_category_display_name(category) if category else "All Categories", - location=location or "Worldwide", - total_count=data.get("total", len(attractions)), - attractions=attractions + return {"error": "No movies found matching the criteria"} + + movies = [] + if "movies" in data: + for item in data["movies"]: + movie = parse_movie_data(item) + movies.append(movie) + + movies_list = MoviesList( + genre=get_genre_display_name(genre) if genre else "All Genres", + total_count=data.get("total", len(movies)), + movies=movies ) - return asdict(attractions_list) + return asdict(movies_list) except Exception as e: - return {"error": f"Failed to search attractions: {str(e)}"} + return {"error": f"Failed to search movies: {str(e)}"} -def get_random_attraction_data(region: str = "famous") -> Dict[str, Any]: - """Get a random attraction +def get_random_movie_data() -> Dict[str, Any]: + """Get a random movie - Args: - region: Region type - "famous" for world famous attractions, "india" for Indian attractions Returns: - Attraction object as dictionary or error dict + Movie object as dictionary or error dict """ try: - if region.lower() == "india": - data = get_random_india_attraction() - else: - data = get_random_famous_attraction() + data = get_random_famous_movie() if not data: - return {"error": f"No random attraction found for region: {region}"} + return {"error": f"No random movie found"} - attraction = parse_attraction_data(data) - return asdict(attraction) + movie = parse_movie_data(data) + return asdict(movie) except Exception as e: - return {"error": f"Failed to get random attraction: {str(e)}"} + return {"error": f"Failed to get random movie: {str(e)}"} -def get_world_wonders_data() -> str: - """Get list of world wonders attractions as formatted string +def get_movie_genres_data() -> str: + """Get list of available movie genres as formatted string Returns: - Formatted string with world wonders attractions + Formatted string with genre codes and display names """ try: - data = get_wonders_of_world() - if not data: - return "Error: No world wonders found" - - attractions = [] - if "attractions" in data: - for item in data["attractions"]: - attraction = parse_attraction_data(item) - attractions.append(attraction) - - result = "🌟 **Wonders of the World**\n\n" - result += f"Total Wonders: {len(attractions)}\n\n" - - for i, attraction in enumerate(attractions, 1): - location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country + result = "🏛️ **Available Movie Genres**\n\n" + result += f"Total Genres: {len(MOVIE_GENRES)}\n\n" - result += f"{i}. 🏛️ **{attraction.name}**\n" - result += f" 📍 {location_str}\n" - result += f" 🏷️ {get_category_display_name(attraction.category)}\n" - - if attraction.rating: - result += f" ⭐ {attraction.rating}/5.0\n" - if attraction.entry_fee: - result += f" 💰 {attraction.entry_fee}\n" - if attraction.description: - result += f" 📝 {attraction.description}\n" - - result += "\n" - - return result - - except Exception as e: - return f"Error: Failed to get world wonders: {str(e)}" - - -def book_attraction_data( - attraction_id: int, - visitor_name: str, - email: str, - visit_date: str, - num_visitors: int = 1, - phone: str = None, - special_requirements: str = None -) -> Dict[str, Any]: - """Book an attraction visit - - Args: - attraction_id: ID of the attraction to book - visitor_name: Name of the primary visitor - email: Email address for booking confirmation - visit_date: Visit date in YYYY-MM-DD format - num_visitors: Number of visitors (default: 1) - phone: Optional phone number - special_requirements: Optional special requirements - - Returns: - BookingResponse object as dictionary or error dict - """ - try: - if not visitor_name.strip(): - return {"error": "Visitor name is required"} - - if not validate_email(email): - return {"error": "Invalid email address"} - - if not validate_visit_date(visit_date): - return {"error": "Visit date must be in the future and in YYYY-MM-DD format"} - - if num_visitors < 1 or num_visitors > 50: - return {"error": "Number of visitors must be between 1 and 50"} - - # Get attraction details for cost calculation - attraction_data = get_attraction_by_id(attraction_id) - if not attraction_data: - return {"error": f"Attraction with ID {attraction_id} not found"} - - attraction = parse_attraction_data(attraction_data) - - # Calculate estimated cost - total_cost = calculate_estimated_cost(num_visitors, attraction.entry_fee) - - # Create booking - booking_id = generate_booking_id() - confirmation_code = generate_confirmation_code() - - booking_response = BookingResponse( - booking_id=booking_id, - attraction_id=attraction_id, - visitor_name=visitor_name, - visit_date=visit_date, - num_visitors=num_visitors, - total_cost=total_cost, - booking_status="confirmed", - confirmation_code=confirmation_code - ) - - return asdict(booking_response) - - except Exception as e: - return {"error": f"Failed to book attraction: {str(e)}"} - - -def get_attraction_categories_data() -> str: - """Get list of available attraction categories as formatted string - - Returns: - Formatted string with category codes and display names - """ - try: - result = "🏛️ **Available Attraction Categories**\n\n" - result += f"Total Categories: {len(ATTRACTION_CATEGORIES)}\n\n" - - for code, display_name in ATTRACTION_CATEGORIES.items(): + for code, display_name in MOVIE_GENRES.items(): result += f"• **{code}**: {display_name}\n" - result += "\n*Use these category codes when searching for attractions.*" + result += "\n*Use these genre codes when searching for movies.*" return result except Exception as e: return f"Error: Failed to get categories: {str(e)}" -def format_attraction_resource(attraction_id: int) -> str: - """Get attraction information as a formatted resource""" - data = get_attraction_details_data(attraction_id) - if "error" in data: - return f"Error: {data['error']}" - - attraction_data = data['attraction'] - attraction = parse_attraction_data(attraction_data) - - return format_attraction_details(attraction) - - -def get_booking_summary_prompt(location: str, category: str = None) -> str: - """Generate a prompt for attraction booking summary""" - base = f"Please provide a summary of top attractions in {location}" - if category: - base += f" focusing on {get_category_display_name(category)} attractions" - base += ", including booking recommendations, best times to visit, and practical travel advice." - return base - - def format_search_results(search_data: Dict[str, Any]) -> str: """Format search results as a readable string""" if "error" in search_data: return f"Error: {search_data['error']}" - attractions = search_data.get('attractions', []) - if not attractions: - return "No attractions found matching your criteria." + movies = search_data.get('movies', []) + if not movies: + return "No movies found matching your criteria." - result = f"🎯 Found {search_data.get('total_count', len(attractions))} attractions" - if search_data.get('location') != "Worldwide": - result += f" in {search_data['location']}" - if search_data.get('category') != "All Categories": - result += f" ({search_data['category']})" + result = f"🎯 Found {search_data.get('total_count', len(movies))} movies" + if search_data.get('genre') != "All Genres": + result += f" ({search_data['genre']})" result += ":\n\n" - for i, attraction_data in enumerate(attractions[:10], 1): # Show first 10 - attraction = parse_attraction_data(attraction_data) - location_str = f"{attraction.location.city}, {attraction.location.country}" if attraction.location.city else attraction.location.country + for i, movie_data in enumerate(movies[:10], 1): # Show first 10 + movie = parse_movie_data(movie_data) - result += f"{i}. 🏛️ **{attraction.name}**\n" - result += f" 📍 {location_str}\n" - result += f" 🏷️ {get_category_display_name(attraction.category)}\n" + result += f"{i}. 🎬 **{movie.title}**\n" + result += f" 🏷️ {get_genre_display_name(movie.genre)}\n" - if attraction.rating: - result += f" ⭐ {attraction.rating}/5.0\n" - if attraction.entry_fee: - result += f" 💰 {attraction.entry_fee}\n" + if movie.rating: + result += f" ⭐ {movie.rating}/5.0\n" + if movie.durationMins: + result += f" ⏳ {movie.durationMins}\n" result += "\n" - if len(attractions) > 10: - result += f"... and {len(attractions) - 10} more attractions\n" + if len(movies) > 10: + result += f"... and {len(movies) - 10} more movies\n" return result + diff --git a/src/mcp/movie-reviews-mcp/pyproject.toml b/src/mcp/movie-reviews-mcp/pyproject.toml index c024fdf..335839c 100644 --- a/src/mcp/movie-reviews-mcp/pyproject.toml +++ b/src/mcp/movie-reviews-mcp/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "attractions-mcp" +name = "movie-reviews-mcp" version = "1.0.0" -description = "MCP server for discovering and booking tourist attractions worldwide" +description = "MCP server for discovering movies and reviews" readme = "README.md" requires-python = ">=3.13" dependencies = [ diff --git a/src/mcp/movie-reviews-mcp/readme.md b/src/mcp/movie-reviews-mcp/readme.md index 5af9573..f1ed6b6 100644 --- a/src/mcp/movie-reviews-mcp/readme.md +++ b/src/mcp/movie-reviews-mcp/readme.md @@ -1,37 +1,20 @@ -# Tourist Attractions MCP Server +get_movie_details_data# Movie Reviews MCP Server -A Model Context Protocol (MCP) server for discovering and booking tourist attractions worldwide using the World Tourist Attractions API. +A Model Context Protocol (MCP) server for discovering movie list and reviews using the Movie Reviews API. ## Features -🏛️ **Attraction Discovery** -- Search attractions by location and category -- Get detailed attraction information -- Discover random famous attractions -- Explore world wonders - -🎫 **Booking System** -- Book attraction visits -- Generate confirmation codes -- Calculate estimated costs -- Validate visit dates and requirements - -🗂️ **Categories Supported** -- Historical Sites -- Natural Wonders -- Cultural Sites -- Religious Sites -- Museums -- Parks & Gardens -- Beaches & Mountains -- Architecture -- Entertainment & Adventure Sports +🏛️ **Movie Discovery** +- Search movies by title and genre +- Get detailed movie information +- Discover a random famous movie +- Get a list of reviews from a movie ## Installation ```bash -## cd to attractions mcp project -cd src/mcp/attractions-mcp +## cd to movie reviews mcp project +cd src/mcp/movie-reviews-mcp ``` ```bash @@ -52,110 +35,69 @@ uv run main.py ### Available Tools -#### 1. Get Attraction Details +#### 1. Get Movie Details ```python -get_attraction_details(attraction_id: int) +get_movie_details_data(movie_id: int) ``` -Get comprehensive information about a specific attraction including facilities, best visiting times, and reviews. +Get comprehensive information about a specific movie and its reviews. -#### 2. Search Attractions +#### 2. Search Movies ```python -search_attractions(location: str = None, category: str = None, limit: int = 20) +search_movies_data(title: str = None, genre: str = None, limit: int = 20) ``` -Search for attractions with optional location and category filters. +Search for movies by title and genre. -#### 3. Random Attraction Discovery +#### 3. Random Famous Movie ```python -get_random_attraction(region: str = "famous") +get_random_famous_movie_data(region: str = "famous") ``` -Get a random attraction for inspiration. Use `region="india"` for Indian attractions. - -#### 4. World Wonders -```python -get_world_wonders() -``` -Get the list of world wonder attractions. - -#### 5. Book Attraction -```python -book_attraction( - attraction_id: int, - visitor_name: str, - email: str, - visit_date: str, # YYYY-MM-DD format - num_visitors: int = 1, - phone: str = None, - special_requirements: str = None -) -``` -Book a visit to an attraction with confirmation. - -#### 6. Get Categories -```python -get_attraction_categories() -``` -Get all available attraction categories for filtering. - -#### 7. Search and Format -```python -search_and_format_attractions(location: str = None, category: str = None, limit: int = 10) -``` -Search attractions and return nicely formatted results. +Get a random famous movie. ### Resources Access attraction data as resources: -- `attraction://{attraction_id}` - Specific attraction details -- `attractions://search/{location}` - Attractions by location -- `attractions://category/{category}` - Attractions by category +- `movie://{movie_id}` - Specific movie details +- `movies://search/{location}` - movies by location +- `movies://genre/{genre}` - movies by category ### Prompts -- `attraction_booking_prompt(location, category)` - Booking assistance -- `travel_planning_prompt(location, days)` - Multi-day itinerary planning -- `attraction_comparison_prompt(attraction_ids)` - Compare multiple attractions +- `movie_booking_prompt(title, genre)` - Booking assistance +- `travel_planning_prompt(title, days)` - Multi-day itinerary planning +- `movie_comparison_prompt(movie_ids)` - Compare multiple movies ## Example Usage ```python -# Search for historical attractions in Rome -search_attractions(location="Rome", category="historical", limit=10) - -# Get details about the Colosseum (example ID: 123) -get_attraction_details(123) +# Search for historical movies in Rome +search_movies(title="Inception", genre="sci-fi", limit=10) -# Book a visit -book_attraction( - attraction_id=123, - visitor_name="John Doe", - email="john@example.com", - visit_date="2024-06-15", - num_visitors=2 -) +# Get details about the movie Inception (example ID: 1) +get_movie_details(1) # Get formatted search results -search_and_format_attractions(location="Paris", category="cultural", limit=5) +search_and_format_movies(location="Paris", category="cultural", limit=5) ``` ## Project Structure ``` src/mcp/attractions-mcp/ -├── __init__.py # Package exports +├── __init__.py # Package exports ├── main.py # MCP server setup and tools -├── models.py # Data classes (Attraction, Booking, etc.) +├── models.py # Data classes (Movie, Booking, etc.) ├── config.py # API URLs and constants ├── utils.py # Helper functions and validation -├── attractions_service.py # Core business logic +├── movie_service.py # Core business logic ├── pyproject.toml # Dependencies -└── README.md # This file +└── README.md # This file ``` ## Development The codebase follows a modular structure similar to the weather-mcp: -- **Models**: Data structures for attractions and bookings -- **Config**: Mock Data for attractions +- **Models**: Data structures for movies and reviews +- **Config**: Mock Data for movies - **Utils**: Helper functions for API calls and validation - **Service**: Business logic and data processing - **Main**: MCP server orchestration diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py index ea29266..c142a1e 100644 --- a/src/mcp/movie-reviews-mcp/utils.py +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -8,8 +8,8 @@ from typing import Dict, Any, Optional, List from datetime import datetime, timedelta -from config import MOVIE_GENRES, MOCK_MOVIES -from models import Coordinates, Location, Attraction +from config import MOVIE_GENRES, MOCK_MOVIES, MOCK_REVIEWS +from models import Movie def make_api_request(url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: @@ -56,128 +56,68 @@ def search_movies( "total": len(filtered_movies) } +def get_random_famous_movie() -> Optional[Dict[str, Any]]: + """Get a random famous movie from mock data""" + if not MOCK_MOVIES: + return None + return random.choice(MOCK_MOVIES) - - -def parse_coordinates(lat: float, lon: float) -> Coordinates: - """Create coordinates object from lat/lon""" - return Coordinates(lat=lat, lon=lon) - - -def parse_location_data(data: Dict[str, Any]) -> Location: - """Parse location data from API response""" - coords = None - if data.get("latitude") and data.get("longitude"): - coords = parse_coordinates(data["latitude"], data["longitude"]) - - return Location( - city=data.get("city", ""), - country=data.get("country", ""), - region=data.get("region", ""), - coordinates=coords - ) - - -def parse_movie_data(data: Dict[str, Any]) -> Attraction: +def parse_movie_data(data: Dict[str, Any]) -> Movie: """Parse movie data from API response""" - location = parse_location_data(data.get("location", {})) - return Attraction( + return Movie( id=data.get("id", 0), name=data.get("name", ""), description=data.get("description", ""), genre=data.get("genre", ""), - location=location, rating=data.get("rating"), - image_url=data.get("image_url"), - website=data.get("website"), - opening_hours=data.get("opening_hours"), - entry_fee=data.get("entry_fee") ) -def format_movie_name(movie: Attraction) -> str: - """Format movie name with location""" - name = movie.name - if movie.location.city: - name += f", {movie.location.city}" - if movie.location.country: - name += f", {movie.location.country}" - return name - - def get_genre_display_name(genre: str) -> str: """Get display name for genre""" - return ATTRACTION_CATEGORIES.get(genre.lower(), genre.title()) + return MOVIE_GENRES.get(genre.lower(), genre.title()) -def generate_booking_id() -> str: - """Generate a unique booking ID""" - timestamp = datetime.now().strftime("%Y%m%d%H%M") - random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - return f"ATT-{timestamp}-{random_part}" +def format_duration(minutes: int) -> str: + """Format duration from minutes to hours and minutes""" + hours = minutes // 60 + mins = minutes % 60 + if hours > 0: + return f"{hours}h {mins}m" + return f"{mins}m" -def generate_confirmation_code() -> str: - """Generate a confirmation code""" - return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) +def get_movie_rating_from_mock_reviews(movie: Movie) -> str: + """Get movie rating from mock reviews""" + ratings = [] -def validate_visit_date(date_str: str) -> bool: - """Validate that visit date is in the future""" - try: - visit_date = datetime.strptime(date_str, "%Y-%m-%d") - return visit_date.date() > datetime.now().date() - except ValueError: - return False - + for review in MOCK_REVIEWS: + if review["movie_id"] == movie.id: + ratings.append(review["rating"]) -def validate_email(email: str) -> bool: - """Basic email validation""" - return "@" in email and "." in email.split("@")[1] + if ratings: + avg_rating = sum(ratings) / len(ratings) + return f"{avg_rating:.1f}/5.0" + return "Not Rated" -def calculate_estimated_cost(num_visitors: int, entry_fee: Optional[str] = None) -> Optional[float]: - """Calculate estimated cost based on number of visitors""" - if not entry_fee or "free" in entry_fee.lower(): - return 0.0 - # Simple cost calculation - in reality this would be more complex - try: - # Extract number from entry fee string (e.g., "$15", "₹500") - import re - numbers = re.findall(r'\d+\.?\d*', entry_fee) - if numbers: - base_cost = float(numbers[0]) - return base_cost * num_visitors - except (ValueError, IndexError): - pass - - return None - - -def format_movie_details(movie: Attraction) -> str: +def format_movie_details(movie: Movie) -> str: """Format movie details as a readable string""" - location_str = f"{movie.location.city}, {movie.location.country}" if movie.location.city else movie.location.country - details = f"🏛️ {movie.name}\n" - details += f"📍 Location: {location_str}\n" - details += f"🏷️ Category: {get_genre_display_name(movie.genre)}\n" + details = f"🎞️ {movie.title}\n" + details += f"🏷️ Genre: {get_genre_display_name(movie.genre)}\n" if movie.rating: - details += f"⭐ Rating: {movie.rating}/5.0\n" - - if movie.opening_hours: - details += f"🕒 Hours: {movie.opening_hours}\n" - - if movie.entry_fee: - details += f"💰 Entry Fee: {movie.entry_fee}\n" + details += f"⭐ Rating: {get_movie_rating_from_mock_reviews(movie)}\n" - if movie.description: - details += f"\n📝 Description: {movie.description}\n" + if movie.synopsis: + details += f"\n📝 Synopsis: {movie.synopsis}\n" - if movie.website: - details += f"🌐 Website: {movie.website}\n" + if movie.durationMins: + details += f"⏳ Duration: {format_duration(movie.durationMins)}\n" return details From 97b9f995f4516f4a6216940fac277e6d5394a6f2 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:03:49 +0100 Subject: [PATCH 13/25] Update cinema.ipynb --- src/agent/cinema.ipynb | 1592 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 1558 insertions(+), 34 deletions(-) diff --git a/src/agent/cinema.ipynb b/src/agent/cinema.ipynb index a0f6a69..ba2554b 100644 --- a/src/agent/cinema.ipynb +++ b/src/agent/cinema.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 27, "id": "a26927c2", "metadata": {}, "outputs": [ @@ -24,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 28, "id": "aba67892", "metadata": {}, "outputs": [ @@ -41,7 +41,7 @@ "output_type": "stream", "text": [ "\u001b[2mUsing Python 3.13.7 environment at: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\u001b[0m\n", - "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 38ms\u001b[0m\u001b[0m\n" + "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 21ms\u001b[0m\u001b[0m\n" ] } ], @@ -54,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 29, "id": "b86c12d0", "metadata": {}, "outputs": [ @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 30, "id": "a60c8345", "metadata": {}, "outputs": [ @@ -123,7 +123,7 @@ " mcp_client = MultiServerMCPClient({\n", " \"cinema\": {\n", " \"transport\": \"streamable_http\",\n", - " \"url\": \"http://localhost:8010\" # Cinema MCP server on port 8010\n", + " \"url\": os.getenv(\"CINEMA_MCP_URL\") # Cinema MCP server with /mcp/ path\n", " }\n", " })\n", " \n", @@ -135,7 +135,7 @@ " except Exception as e:\n", " print(f\"Error connecting to Cinema MCP HTTP server: {e}\")\n", " print(\"Make sure the cinema MCP server is running on port 8010\")\n", - " print(\"Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\")\n", + " print(\"Run: cd src/mcp/cinema-mcp && uv run main.py\")\n", " return []\n", "\n", "print(\"🔗 Cinema MCP HTTP adapter setup ready!\")" @@ -143,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 31, "id": "57e4f778", "metadata": {}, "outputs": [ @@ -164,7 +164,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 32, "id": "501c5c58", "metadata": {}, "outputs": [ @@ -247,7 +247,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 33, "id": "688da57b", "metadata": {}, "outputs": [ @@ -256,16 +256,12 @@ "output_type": "stream", "text": [ "Initializing cinema agent with MCP tools...\n", - "Error connecting to Cinema MCP HTTP server: unhandled errors in a TaskGroup (1 sub-exception)\n", - "Make sure the cinema MCP server is running on port 8010\n", - "Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\n", - "No MCP tools loaded. Make sure the Cinema MCP server is accessible.\n", - "Failed to initialize agent. Check Cinema MCP server connection.\n", - "Error connecting to Cinema MCP HTTP server: unhandled errors in a TaskGroup (1 sub-exception)\n", - "Make sure the cinema MCP server is running on port 8010\n", - "Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\n", - "No MCP tools loaded. Make sure the Cinema MCP server is accessible.\n", - "Failed to initialize agent. Check Cinema MCP server connection.\n" + "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "LangChain cinema agent with MCP tools ready!\n", + "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "LangChain cinema agent with MCP tools ready!\n" ] } ], @@ -287,7 +283,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 34, "id": "dc7277da", "metadata": {}, "outputs": [ @@ -351,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 35, "id": "3c70994e", "metadata": {}, "outputs": [ @@ -359,12 +355,82 @@ "name": "stdout", "output_type": "stream", "text": [ - "Error connecting to Cinema MCP HTTP server: unhandled errors in a TaskGroup (1 sub-exception)\n", - "Make sure the cinema MCP server is running on port 8010\n", - "Run: cd src/mcp/cinema-mcp && uv run mcp dev main.py\n", - "Failed to connect to Cinema MCP HTTP server\n", - "Make sure to start the cinema MCP server first:\n", - "cd src/mcp/cinema-mcp && uv run mcp dev main.py\n" + "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "Cinema MCP HTTP server connected successfully!\n", + "Available tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + " - get_current_movies: Get all currently playing movies with their showtimes and availability\n", + "\n", + "Returns:\n", + " List of all movies currently being shown with details including:\n", + " - Movie title, description, and basic info\n", + " - Showtimes and theater room assignments \n", + " - Seat availability and pricing\n", + " - Genre, rating, and duration\n", + " - Cast and director information\n", + "\n", + " - get_movie_details: Get detailed information about a specific movie showing\n", + "\n", + "Args:\n", + " title: Exact movie title\n", + " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " time: Time in HH:MM format (e.g., \"19:30\")\n", + " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", + " \n", + "Returns:\n", + " Detailed movie information including plot, cast, theater details, and availability\n", + " Use search_movies first to find the exact title, date, time, and room values needed\n", + "\n", + " - search_movies: Search for movie presentations with optional filters\n", + "\n", + "Args:\n", + " title: Filter by movie title (partial match, case-insensitive)\n", + " genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family)\n", + " date: Filter by date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " room: Filter by cinema room (theater_a, theater_b, theater_c, imax)\n", + " available_seats_min: Minimum number of available seats required\n", + " limit: Maximum number of results to return (default: 20, max: 100)\n", + " \n", + "Returns:\n", + " Filtered list of movie presentations matching the search criteria\n", + "\n", + " - make_reservation: Create a movie reservation for a specific showing\n", + "\n", + "Args:\n", + " title: Exact movie title (use get_movie_details or search_movies to find exact title)\n", + " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " time: Time in HH:MM format (e.g., \"19:30\")\n", + " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", + " seats_count: Number of seats to reserve (must be > 0)\n", + " customer_name: Customer's full name\n", + " customer_email: Customer's email address\n", + " customer_phone: Customer's phone number (optional)\n", + " special_requests: Any special requests or accessibility needs (optional)\n", + " \n", + "Returns:\n", + " Reservation confirmation with booking details and pricing information\n", + " Use the exact title, date, time, and room values from search_movies or get_current_movies results\n", + "\n", + " - get_my_reservations: Get all reservations for a customer\n", + "\n", + "Args:\n", + " customer_email: Customer's email address used for reservations\n", + " \n", + "Returns:\n", + " List of all reservations made by the customer\n", + "\n", + " - cancel_reservation: Cancel a movie reservation\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation to cancel\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " \n", + "Returns:\n", + " Cancellation confirmation and details about freed seats\n", + " Use get_my_reservations first to find the exact details of reservations to cancel\n", + "\n" ] } ], @@ -389,7 +455,96 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 36, + "id": "de62dd60", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🔍 Debugging Cinema MCP connection...\n", + "✅ Basic HTTP connection works: 404\n", + "✅ MCP client created successfully\n", + "✅ Basic HTTP connection works: 404\n", + "✅ MCP client created successfully\n", + "✅ Got 6 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", + "Full traceback:\n", + "✅ Got 6 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", + "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", + "Full traceback:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Traceback (most recent call last):\n", + " File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_27352\\2795222468.py\", line 37, in debug_mcp_connection\n", + " await client.close()\n", + " ^^^^^^^^^^^^\n", + "AttributeError: 'MultiServerMCPClient' object has no attribute 'close'\n" + ] + } + ], + "source": [ + "# Detailed debug of MCP connection\n", + "import traceback\n", + "import httpx\n", + "\n", + "async def debug_mcp_connection():\n", + " \"\"\"Debug the Cinema MCP connection with detailed error info\"\"\"\n", + " print(\"🔍 Debugging Cinema MCP connection...\")\n", + " \n", + " # Test basic HTTP connectivity first\n", + " try:\n", + " async with httpx.AsyncClient() as client:\n", + " response = await client.get(\"http://localhost:8010\", timeout=5.0)\n", + " print(f\"✅ Basic HTTP connection works: {response.status_code}\")\n", + " except Exception as e:\n", + " print(f\"❌ Basic HTTP connection failed: {e}\")\n", + " return\n", + " \n", + " # Test MCP client creation with detailed error tracking\n", + " try:\n", + " from langchain_mcp_adapters.client import MultiServerMCPClient\n", + " \n", + " client = MultiServerMCPClient({\n", + " \"cinema\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": os.getenv(\"CINEMA_MCP_URL\")\n", + " }\n", + " })\n", + " \n", + " print(\"✅ MCP client created successfully\")\n", + " \n", + " # Test getting tools with full error details\n", + " try:\n", + " tools = await client.get_tools()\n", + " print(f\"✅ Got {len(tools)} tools: {[tool.name for tool in tools]}\")\n", + " \n", + " # Test closing connection\n", + " await client.close()\n", + " print(\"✅ Connection closed successfully\")\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error getting tools: {e}\")\n", + " print(\"Full traceback:\")\n", + " traceback.print_exc()\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error creating MCP client: {e}\")\n", + " print(\"Full traceback:\")\n", + " traceback.print_exc()\n", + "\n", + "# Run detailed debug\n", + "await debug_mcp_connection()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, "id": "1551f227", "metadata": {}, "outputs": [ @@ -399,7 +554,614 @@ "text": [ "🎬 User: What movies are currently playing?\n", "🤖 Assistant:\n", - "Agent not initialized. Please run the initialization cell first.\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "responded: Sure! Let’s take a look at all the movies currently playing along with their showtimes and availability.\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "responded: Sure! Let’s take a look at all the movies currently playing along with their showtimes and availability.\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 31,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 69.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 31,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 69.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere’s what’s currently playing at **MovieMagic Cinema**:\n", + "\n", + "---\n", + "\n", + "### 🎬 **Today – September 25, 2025**\n", + "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", + " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", + " 🎟 $12.50 per seat | Seats left: 105/150 \n", + " *An epic space journey to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **The Midnight Mystery** *(Thriller, R)* \n", + " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", + " 🎟 $14.00 per seat | Seats left: 75/200 \n", + " *A small-town sheriff unravels a string of midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", + " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", + " 🎟 $12.50 per seat | Seats left: 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **Dragon's Heart** *(Animation, G)* \n", + " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", + " 🎟 $10.00 per seat | Seats left: 77/100 \n", + " *A young girl who can speak to dragons must save her village.* \n", + " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "\n", + "5. **City of Shadows** *(Drama, R)* \n", + " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", + " 🎟 $14.00 per seat | Seats left: 44/200 \n", + " *A noir mystery set in 1940s New York about corruption and murder.* \n", + " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", + "\n", + "6. **Ocean's Edge** *(Documentary, G)* \n", + " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", + " 🎟 $18.00 per seat | Seats left: 222/300 \n", + " *Exploring the ocean’s depths and its marvelous creatures.* \n", + " **Narrator:** David Attenborough\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **Love in Paris** *(Romance, PG)* \n", + " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", + " 🎟 $11.50 per seat | Seats left: 31/100 \n", + " *An American tourist finds romance in Paris with a café owner.* \n", + " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "\n", + "8. **Nightmare Manor** *(Horror, R)* \n", + " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", + " 🎟 $13.00 per seat | Seats left: 58/150 \n", + " *A haunted mansion filled with the ghosts of past owners.* \n", + " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "\n", + "---\n", + "\n", + "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here’s what’s currently playing at **MovieMagic Cinema**:\n", + "\n", + "---\n", + "\n", + "### 🎬 **Today – September 25, 2025**\n", + "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", + " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", + " 🎟 $12.50 per seat | Seats left: 105/150 \n", + " *An epic space journey to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **The Midnight Mystery** *(Thriller, R)* \n", + " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", + " 🎟 $14.00 per seat | Seats left: 75/200 \n", + " *A small-town sheriff unravels a string of midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", + " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", + " 🎟 $12.50 per seat | Seats left: 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **Dragon's Heart** *(Animation, G)* \n", + " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", + " 🎟 $10.00 per seat | Seats left: 77/100 \n", + " *A young girl who can speak to dragons must save her village.* \n", + " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "\n", + "5. **City of Shadows** *(Drama, R)* \n", + " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", + " 🎟 $14.00 per seat | Seats left: 44/200 \n", + " *A noir mystery set in 1940s New York about corruption and murder.* \n", + " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", + "\n", + "6. **Ocean's Edge** *(Documentary, G)* \n", + " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", + " 🎟 $18.00 per seat | Seats left: 222/300 \n", + " *Exploring the ocean’s depths and its marvelous creatures.* \n", + " **Narrator:** David Attenborough\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **Love in Paris** *(Romance, PG)* \n", + " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", + " 🎟 $11.50 per seat | Seats left: 31/100 \n", + " *An American tourist finds romance in Paris with a café owner.* \n", + " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "\n", + "8. **Nightmare Manor** *(Horror, R)* \n", + " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", + " 🎟 $13.00 per seat | Seats left: 58/150 \n", + " *A haunted mansion filled with the ghosts of past owners.* \n", + " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "\n", + "---\n", + "\n", + "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\n", + "\n", + "==================================================\n", + "\n", + "\u001b[32;1m\u001b[1;3mHere’s what’s currently playing at **MovieMagic Cinema**:\n", + "\n", + "---\n", + "\n", + "### 🎬 **Today – September 25, 2025**\n", + "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", + " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", + " 🎟 $12.50 per seat | Seats left: 105/150 \n", + " *An epic space journey to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **The Midnight Mystery** *(Thriller, R)* \n", + " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", + " 🎟 $14.00 per seat | Seats left: 75/200 \n", + " *A small-town sheriff unravels a string of midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", + " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", + " 🎟 $12.50 per seat | Seats left: 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **Dragon's Heart** *(Animation, G)* \n", + " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", + " 🎟 $10.00 per seat | Seats left: 77/100 \n", + " *A young girl who can speak to dragons must save her village.* \n", + " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "\n", + "5. **City of Shadows** *(Drama, R)* \n", + " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", + " 🎟 $14.00 per seat | Seats left: 44/200 \n", + " *A noir mystery set in 1940s New York about corruption and murder.* \n", + " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", + "\n", + "6. **Ocean's Edge** *(Documentary, G)* \n", + " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", + " 🎟 $18.00 per seat | Seats left: 222/300 \n", + " *Exploring the ocean’s depths and its marvelous creatures.* \n", + " **Narrator:** David Attenborough\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **Love in Paris** *(Romance, PG)* \n", + " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", + " 🎟 $11.50 per seat | Seats left: 31/100 \n", + " *An American tourist finds romance in Paris with a café owner.* \n", + " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "\n", + "8. **Nightmare Manor** *(Horror, R)* \n", + " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", + " 🎟 $13.00 per seat | Seats left: 58/150 \n", + " *A haunted mansion filled with the ghosts of past owners.* \n", + " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "\n", + "---\n", + "\n", + "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here’s what’s currently playing at **MovieMagic Cinema**:\n", + "\n", + "---\n", + "\n", + "### 🎬 **Today – September 25, 2025**\n", + "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", + " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", + " 🎟 $12.50 per seat | Seats left: 105/150 \n", + " *An epic space journey to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **The Midnight Mystery** *(Thriller, R)* \n", + " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", + " 🎟 $14.00 per seat | Seats left: 75/200 \n", + " *A small-town sheriff unravels a string of midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", + " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", + " 🎟 $12.50 per seat | Seats left: 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **Dragon's Heart** *(Animation, G)* \n", + " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", + " 🎟 $10.00 per seat | Seats left: 77/100 \n", + " *A young girl who can speak to dragons must save her village.* \n", + " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "\n", + "5. **City of Shadows** *(Drama, R)* \n", + " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", + " 🎟 $14.00 per seat | Seats left: 44/200 \n", + " *A noir mystery set in 1940s New York about corruption and murder.* \n", + " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", + "\n", + "6. **Ocean's Edge** *(Documentary, G)* \n", + " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", + " 🎟 $18.00 per seat | Seats left: 222/300 \n", + " *Exploring the ocean’s depths and its marvelous creatures.* \n", + " **Narrator:** David Attenborough\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **Love in Paris** *(Romance, PG)* \n", + " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", + " 🎟 $11.50 per seat | Seats left: 31/100 \n", + " *An American tourist finds romance in Paris with a café owner.* \n", + " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "\n", + "8. **Nightmare Manor** *(Horror, R)* \n", + " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", + " 🎟 $13.00 per seat | Seats left: 58/150 \n", + " *A haunted mansion filled with the ghosts of past owners.* \n", + " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "\n", + "---\n", + "\n", + "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\n", "\n", "==================================================\n", "\n" @@ -420,7 +1182,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "id": "9295d748", "metadata": {}, "outputs": [ @@ -431,7 +1193,761 @@ "🎬 Welcome to MovieMagic Cinema Assistant!\n", "I can help you find movies, check showtimes, and make reservations.\n", "Type 'exit' to quit. Press Enter on an empty line to skip.\n", - "\n" + "\n", + "🎬 User: show me recent movies\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: show me recent movies\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 31,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 69.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 31,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 69.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the **recent movies** currently playing at MovieMagic Cinema:\n", + "\n", + "---\n", + "\n", + "### 📅 **Today – September 25, 2025**\n", + "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", + " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 14:30 — **Seats left:** 105/150 \n", + " *Epic space adventure to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", + " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 19:15 — **Seats left:** 75/200 \n", + " *Small-town sheriff investigates mysterious midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", + " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 21:45 — **Seats left:** 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **🐉 Dragon's Heart** *(Animation, G)* \n", + " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", + " 🕒 10:00 — **Seats left:** 77/100 \n", + " *Girl who communicates with dragons must save her village.* \n", + "\n", + "5. **🏙 City of Shadows** *(Drama, R)* \n", + " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 16:20 — **Seats left:** 44/200 \n", + " *1940s noir mystery of murder and corruption.* \n", + "\n", + "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", + " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", + " 🕒 20:00 — **Seats left:** 222/300 \n", + " *Exploring the ocean’s depths narrated by David Attenborough.*\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **💖 Love in Paris** *(Romance, PG)* \n", + " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", + " 🕒 15:45 — **Seats left:** 31/100 \n", + " *American tourist finds love in Paris.* \n", + "\n", + "8. **👻 Nightmare Manor** *(Horror, R)* \n", + " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", + " 🕒 22:30 — **Seats left:** 58/150 \n", + " *A haunted mansion hides the spirits of its past owners.*\n", + "\n", + "---\n", + "\n", + "Which one would you like me to get **more details on** or help you **book tickets** for?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the **recent movies** currently playing at MovieMagic Cinema:\n", + "\n", + "---\n", + "\n", + "### 📅 **Today – September 25, 2025**\n", + "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", + " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 14:30 — **Seats left:** 105/150 \n", + " *Epic space adventure to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", + " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 19:15 — **Seats left:** 75/200 \n", + " *Small-town sheriff investigates mysterious midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", + " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 21:45 — **Seats left:** 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **🐉 Dragon's Heart** *(Animation, G)* \n", + " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", + " 🕒 10:00 — **Seats left:** 77/100 \n", + " *Girl who communicates with dragons must save her village.* \n", + "\n", + "5. **🏙 City of Shadows** *(Drama, R)* \n", + " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 16:20 — **Seats left:** 44/200 \n", + " *1940s noir mystery of murder and corruption.* \n", + "\n", + "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", + " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", + " 🕒 20:00 — **Seats left:** 222/300 \n", + " *Exploring the ocean’s depths narrated by David Attenborough.*\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **💖 Love in Paris** *(Romance, PG)* \n", + " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", + " 🕒 15:45 — **Seats left:** 31/100 \n", + " *American tourist finds love in Paris.* \n", + "\n", + "8. **👻 Nightmare Manor** *(Horror, R)* \n", + " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", + " 🕒 22:30 — **Seats left:** 58/150 \n", + " *A haunted mansion hides the spirits of its past owners.*\n", + "\n", + "---\n", + "\n", + "Which one would you like me to get **more details on** or help you **book tickets** for?\n", + "\u001b[32;1m\u001b[1;3mHere are the **recent movies** currently playing at MovieMagic Cinema:\n", + "\n", + "---\n", + "\n", + "### 📅 **Today – September 25, 2025**\n", + "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", + " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 14:30 — **Seats left:** 105/150 \n", + " *Epic space adventure to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", + " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 19:15 — **Seats left:** 75/200 \n", + " *Small-town sheriff investigates mysterious midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", + " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 21:45 — **Seats left:** 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **🐉 Dragon's Heart** *(Animation, G)* \n", + " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", + " 🕒 10:00 — **Seats left:** 77/100 \n", + " *Girl who communicates with dragons must save her village.* \n", + "\n", + "5. **🏙 City of Shadows** *(Drama, R)* \n", + " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 16:20 — **Seats left:** 44/200 \n", + " *1940s noir mystery of murder and corruption.* \n", + "\n", + "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", + " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", + " 🕒 20:00 — **Seats left:** 222/300 \n", + " *Exploring the ocean’s depths narrated by David Attenborough.*\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **💖 Love in Paris** *(Romance, PG)* \n", + " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", + " 🕒 15:45 — **Seats left:** 31/100 \n", + " *American tourist finds love in Paris.* \n", + "\n", + "8. **👻 Nightmare Manor** *(Horror, R)* \n", + " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", + " 🕒 22:30 — **Seats left:** 58/150 \n", + " *A haunted mansion hides the spirits of its past owners.*\n", + "\n", + "---\n", + "\n", + "Which one would you like me to get **more details on** or help you **book tickets** for?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the **recent movies** currently playing at MovieMagic Cinema:\n", + "\n", + "---\n", + "\n", + "### 📅 **Today – September 25, 2025**\n", + "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", + " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 14:30 — **Seats left:** 105/150 \n", + " *Epic space adventure to save humanity from alien threats.* \n", + " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", + "\n", + "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", + " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 19:15 — **Seats left:** 75/200 \n", + " *Small-town sheriff investigates mysterious midnight disappearances.* \n", + " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", + "\n", + "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", + " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", + " 🕒 21:45 — **Seats left:** 61/150 \n", + " *Three friends accidentally become viral sensations.* \n", + " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "\n", + "---\n", + "\n", + "### 📅 **Tomorrow – September 26, 2025**\n", + "4. **🐉 Dragon's Heart** *(Animation, G)* \n", + " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", + " 🕒 10:00 — **Seats left:** 77/100 \n", + " *Girl who communicates with dragons must save her village.* \n", + "\n", + "5. **🏙 City of Shadows** *(Drama, R)* \n", + " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", + " 🕒 16:20 — **Seats left:** 44/200 \n", + " *1940s noir mystery of murder and corruption.* \n", + "\n", + "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", + " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", + " 🕒 20:00 — **Seats left:** 222/300 \n", + " *Exploring the ocean’s depths narrated by David Attenborough.*\n", + "\n", + "---\n", + "\n", + "### 📅 **September 27, 2025**\n", + "7. **💖 Love in Paris** *(Romance, PG)* \n", + " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", + " 🕒 15:45 — **Seats left:** 31/100 \n", + " *American tourist finds love in Paris.* \n", + "\n", + "8. **👻 Nightmare Manor** *(Horror, R)* \n", + " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", + " 🕒 22:30 — **Seats left:** 58/150 \n", + " *A haunted mansion hides the spirits of its past owners.*\n", + "\n", + "---\n", + "\n", + "Which one would you like me to get **more details on** or help you **book tickets** for?\n", + "🎬 User: I want to reserve one seat for Laugh Our Loud on 25th september, one seat, my name is Andrzej Pytel, contact email kontakt@andrzejpytel.pl\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: I want to reserve one seat for Laugh Our Loud on 25th september, one seat, my name is Andrzej Pytel, contact email kontakt@andrzejpytel.pl\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `make_reservation` with `{'title': 'Laugh Out Loud', 'date': '2025-09-25', 'time': '21:45', 'room': 'theater_a', 'seats_count': 1, 'customer_name': 'Andrzej Pytel', 'customer_email': 'kontakt@andrzejpytel.pl'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `make_reservation` with `{'title': 'Laugh Out Loud', 'date': '2025-09-25', 'time': '21:45', 'room': 'theater_a', 'seats_count': 1, 'customer_name': 'Andrzej Pytel', 'customer_email': 'kontakt@andrzejpytel.pl'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[31;1m\u001b[1;3m{\n", + " \"confirmation\": {\n", + " \"reservation_id\": 3,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_datetime\": \"2025-09-24T14:02:09.153700\"\n", + " },\n", + " \"movie_details\": {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"genre\": \"Comedy\",\n", + " \"duration_minutes\": 95\n", + " },\n", + " \"booking_details\": {\n", + " \"seats_reserved\": 1,\n", + " \"customer_name\": \"Andrzej Pytel\",\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"customer_phone\": null,\n", + " \"special_requests\": null\n", + " },\n", + " \"pricing\": {\n", + " \"price_per_seat\": 12.5,\n", + " \"total_price\": 12.5\n", + " }\n", + "}\u001b[0m\u001b[31;1m\u001b[1;3m{\n", + " \"confirmation\": {\n", + " \"reservation_id\": 3,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_datetime\": \"2025-09-24T14:02:09.153700\"\n", + " },\n", + " \"movie_details\": {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"genre\": \"Comedy\",\n", + " \"duration_minutes\": 95\n", + " },\n", + " \"booking_details\": {\n", + " \"seats_reserved\": 1,\n", + " \"customer_name\": \"Andrzej Pytel\",\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"customer_phone\": null,\n", + " \"special_requests\": null\n", + " },\n", + " \"pricing\": {\n", + " \"price_per_seat\": 12.5,\n", + " \"total_price\": 12.5\n", + " }\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3m✅ **Reservation Confirmed!** \n", + "\n", + "Here are your booking details:\n", + "\n", + "---\n", + "\n", + "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", + "📅 **Date:** September 25, 2025 \n", + "🕒 **Time:** 21:45 \n", + "📍 **Room:** Theater A \n", + "⏳ **Duration:** 95 minutes \n", + "\n", + "👤 **Name:** Andrzej Pytel \n", + "📧 **Email:** kontakt@andrzejpytel.pl \n", + "🎟 **Seats Reserved:** 1 \n", + "💰 **Price Per Seat:** $12.50 \n", + "💵 **Total Price:** $12.50 \n", + "\n", + "---\n", + "\n", + "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", + "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "✅ **Reservation Confirmed!** \n", + "\n", + "Here are your booking details:\n", + "\n", + "---\n", + "\n", + "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", + "📅 **Date:** September 25, 2025 \n", + "🕒 **Time:** 21:45 \n", + "📍 **Room:** Theater A \n", + "⏳ **Duration:** 95 minutes \n", + "\n", + "👤 **Name:** Andrzej Pytel \n", + "📧 **Email:** kontakt@andrzejpytel.pl \n", + "🎟 **Seats Reserved:** 1 \n", + "💰 **Price Per Seat:** $12.50 \n", + "💵 **Total Price:** $12.50 \n", + "\n", + "---\n", + "\n", + "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", + "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\n", + "\u001b[32;1m\u001b[1;3m✅ **Reservation Confirmed!** \n", + "\n", + "Here are your booking details:\n", + "\n", + "---\n", + "\n", + "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", + "📅 **Date:** September 25, 2025 \n", + "🕒 **Time:** 21:45 \n", + "📍 **Room:** Theater A \n", + "⏳ **Duration:** 95 minutes \n", + "\n", + "👤 **Name:** Andrzej Pytel \n", + "📧 **Email:** kontakt@andrzejpytel.pl \n", + "🎟 **Seats Reserved:** 1 \n", + "💰 **Price Per Seat:** $12.50 \n", + "💵 **Total Price:** $12.50 \n", + "\n", + "---\n", + "\n", + "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", + "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "✅ **Reservation Confirmed!** \n", + "\n", + "Here are your booking details:\n", + "\n", + "---\n", + "\n", + "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", + "📅 **Date:** September 25, 2025 \n", + "🕒 **Time:** 21:45 \n", + "📍 **Room:** Theater A \n", + "⏳ **Duration:** 95 minutes \n", + "\n", + "👤 **Name:** Andrzej Pytel \n", + "📧 **Email:** kontakt@andrzejpytel.pl \n", + "🎟 **Seats Reserved:** 1 \n", + "💰 **Price Per Seat:** $12.50 \n", + "💵 **Total Price:** $12.50 \n", + "\n", + "---\n", + "\n", + "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", + "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\n", + "🎬 Thanks for using MovieMagic Cinema! Goodbye!\n", + "🎬 Thanks for using MovieMagic Cinema! Goodbye!\n" ] } ], @@ -470,10 +1986,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "e70dad2d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🧹 Cleanup function ready!\n" + ] + } + ], "source": [ "# Cleanup function for HTTP MCP client\n", "async def cleanup_mcp():\n", From 5e278478bc169e8f073c935745e269c7f028501f Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 14:15:46 +0100 Subject: [PATCH 14/25] Update utils.py --- src/mcp/movie-reviews-mcp/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py index c142a1e..ef6f77f 100644 --- a/src/mcp/movie-reviews-mcp/utils.py +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -45,7 +45,12 @@ def search_movies( if genre: genre_match = movie["genre"] == genre.lower() - if genre_match: + # Filter by title + title_match = True + if title: + title_match = movie["title"] == title.lower() + + if genre_match and title_match: filtered_movies.append(movie) # Apply limit @@ -68,7 +73,7 @@ def parse_movie_data(data: Dict[str, Any]) -> Movie: return Movie( id=data.get("id", 0), - name=data.get("name", ""), + title=data.get("title", ""), description=data.get("description", ""), genre=data.get("genre", ""), rating=data.get("rating"), From 2006969f87debb7fe868ed0eeed2e3244676ad53 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:28:47 +0100 Subject: [PATCH 15/25] Cinema employee tools --- src/agent/cinema.ipynb | 1690 ++++++++++++-------------- src/mcp/cinema-mcp/cinema_service.py | 216 +++- src/mcp/cinema-mcp/main.py | 66 +- 3 files changed, 1082 insertions(+), 890 deletions(-) diff --git a/src/agent/cinema.ipynb b/src/agent/cinema.ipynb index ba2554b..aa5ac13 100644 --- a/src/agent/cinema.ipynb +++ b/src/agent/cinema.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 27, + "execution_count": 40, "id": "a26927c2", "metadata": {}, "outputs": [ @@ -24,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 41, "id": "aba67892", "metadata": {}, "outputs": [ @@ -41,7 +41,7 @@ "output_type": "stream", "text": [ "\u001b[2mUsing Python 3.13.7 environment at: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\u001b[0m\n", - "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 21ms\u001b[0m\u001b[0m\n" + "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 22ms\u001b[0m\u001b[0m\n" ] } ], @@ -54,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 42, "id": "b86c12d0", "metadata": {}, "outputs": [ @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 43, "id": "a60c8345", "metadata": {}, "outputs": [ @@ -143,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 44, "id": "57e4f778", "metadata": {}, "outputs": [ @@ -164,7 +164,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 45, "id": "501c5c58", "metadata": {}, "outputs": [ @@ -247,7 +247,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 46, "id": "688da57b", "metadata": {}, "outputs": [ @@ -283,7 +283,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 47, "id": "dc7277da", "metadata": {}, "outputs": [ @@ -347,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 48, "id": "3c70994e", "metadata": {}, "outputs": [ @@ -455,7 +455,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 49, "id": "de62dd60", "metadata": {}, "outputs": [ @@ -466,11 +466,11 @@ "🔍 Debugging Cinema MCP connection...\n", "✅ Basic HTTP connection works: 404\n", "✅ MCP client created successfully\n", - "✅ Basic HTTP connection works: 404\n", - "✅ MCP client created successfully\n", "✅ Got 6 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", "Full traceback:\n", + "✅ Basic HTTP connection works: 404\n", + "✅ MCP client created successfully\n", "✅ Got 6 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", "Full traceback:\n" @@ -544,7 +544,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 50, "id": "1551f227", "metadata": {}, "outputs": [ @@ -559,11 +559,11 @@ "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", "\u001b[32;1m\u001b[1;3m\n", "Invoking: `get_current_movies` with `{}`\n", - "responded: Sure! Let’s take a look at all the movies currently playing along with their showtimes and availability.\n", + "responded: Sure! Let me check the list of movies currently playing along with their showtimes and availability for you.\n", "\n", "\u001b[0m\u001b[32;1m\u001b[1;3m\n", "Invoking: `get_current_movies` with `{}`\n", - "responded: Sure! Let’s take a look at all the movies currently playing along with their showtimes and availability.\n", + "responded: Sure! Let me check the list of movies currently playing along with their showtimes and availability for you.\n", "\n", "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", " \"cinema_name\": \"MovieMagic Cinema\",\n", @@ -617,7 +617,7 @@ " \"date\": \"2025-09-25\",\n", " \"time\": \"21:45\",\n", " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 61,\n", + " \"seats_remaining\": 60,\n", " \"seats_total\": 150,\n", " \"duration_minutes\": 95,\n", " \"genre\": \"Comedy\",\n", @@ -630,7 +630,7 @@ " \"David Brown\"\n", " ],\n", " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 59.3\n", + " \"occupancy_percentage\": 60.0\n", " },\n", " {\n", " \"title\": \"Dragon's Heart\",\n", @@ -788,7 +788,7 @@ " \"date\": \"2025-09-25\",\n", " \"time\": \"21:45\",\n", " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 61,\n", + " \"seats_remaining\": 60,\n", " \"seats_total\": 150,\n", " \"duration_minutes\": 95,\n", " \"genre\": \"Comedy\",\n", @@ -801,7 +801,7 @@ " \"David Brown\"\n", " ],\n", " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 59.3\n", + " \"occupancy_percentage\": 60.0\n", " },\n", " {\n", " \"title\": \"Dragon's Heart\",\n", @@ -909,259 +909,283 @@ " ]\n", "}\u001b[0m\u001b[32;1m\u001b[1;3mHere’s what’s currently playing at **MovieMagic Cinema**:\n", "\n", - "---\n", - "\n", - "### 🎬 **Today – September 25, 2025**\n", - "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", - " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", - " 🎟 $12.50 per seat | Seats left: 105/150 \n", - " *An epic space journey to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **The Midnight Mystery** *(Thriller, R)* \n", - " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", - " 🎟 $14.00 per seat | Seats left: 75/200 \n", - " *A small-town sheriff unravels a string of midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", - " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", - " 🎟 $12.50 per seat | Seats left: 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "### 🎬 **Today — September 25, 2025**\n", + "1. **Galactic Adventures** \n", + " *Science Fiction | PG-13 | 142 min* \n", + " ⏰ 14:30 — Theater A \n", + " 💺 105 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Epic space journey to save humanity from an alien threat.*\n", + "\n", + "2. **The Midnight Mystery** \n", + " *Thriller | R | 118 min* \n", + " ⏰ 19:15 — Theater B \n", + " 💺 75 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Small town sheriff investigates midnight disappearances.*\n", + "\n", + "3. **Laugh Out Loud** \n", + " *Comedy | PG-13 | 95 min* \n", + " ⏰ 21:45 — Theater A \n", + " 💺 60 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Three friends accidentally become viral internet stars.*\n", "\n", "---\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **Dragon's Heart** *(Animation, G)* \n", - " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", - " 🎟 $10.00 per seat | Seats left: 77/100 \n", - " *A young girl who can speak to dragons must save her village.* \n", - " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", - "\n", - "5. **City of Shadows** *(Drama, R)* \n", - " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", - " 🎟 $14.00 per seat | Seats left: 44/200 \n", - " *A noir mystery set in 1940s New York about corruption and murder.* \n", - " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", - "\n", - "6. **Ocean's Edge** *(Documentary, G)* \n", - " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", - " 🎟 $18.00 per seat | Seats left: 222/300 \n", - " *Exploring the ocean’s depths and its marvelous creatures.* \n", - " **Narrator:** David Attenborough\n", + "### 📅 **September 26, 2025**\n", + "4. **Dragon's Heart** \n", + " *Animation | G | 103 min* \n", + " ⏰ 10:00 — Theater C \n", + " 💺 77 / 100 seats available \n", + " 🎟 $10.00 per seat \n", + " *Young girl must save her village with help from dragons.*\n", + "\n", + "5. **City of Shadows** \n", + " *Drama | R | 134 min* \n", + " ⏰ 16:20 — Theater B \n", + " 💺 44 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Noir detective uncovers police corruption in 1940s NYC.*\n", + "\n", + "6. **Ocean's Edge** \n", + " *Documentary | G | 87 min* \n", + " ⏰ 20:00 — IMAX Theater \n", + " 💺 222 / 300 seats available \n", + " 🎟 $18.00 per seat \n", + " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", "\n", "---\n", "\n", "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** *(Romance, PG)* \n", - " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", - " 🎟 $11.50 per seat | Seats left: 31/100 \n", - " *An American tourist finds romance in Paris with a café owner.* \n", - " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", - "\n", - "8. **Nightmare Manor** *(Horror, R)* \n", - " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", - " 🎟 $13.00 per seat | Seats left: 58/150 \n", - " *A haunted mansion filled with the ghosts of past owners.* \n", - " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "7. **Love in Paris** \n", + " *Romance | PG | 108 min* \n", + " ⏰ 15:45 — Theater C \n", + " 💺 31 / 100 seats available \n", + " 🎟 $11.50 per seat \n", + " *American tourist finds love with a Paris café owner.*\n", + "\n", + "8. **Nightmare Manor** \n", + " *Horror | R | 106 min* \n", + " ⏰ 22:30 — Theater A \n", + " 💺 58 / 150 seats available \n", + " 🎟 $13.00 per seat \n", + " *Family inherits a haunted mansion with dark secrets.*\n", "\n", "---\n", "\n", - "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\u001b[0m\n", + "Would you like me to show more details for any of these movies or help you make a reservation?\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n", "Here’s what’s currently playing at **MovieMagic Cinema**:\n", "\n", - "---\n", - "\n", - "### 🎬 **Today – September 25, 2025**\n", - "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", - " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", - " 🎟 $12.50 per seat | Seats left: 105/150 \n", - " *An epic space journey to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **The Midnight Mystery** *(Thriller, R)* \n", - " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", - " 🎟 $14.00 per seat | Seats left: 75/200 \n", - " *A small-town sheriff unravels a string of midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", - " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", - " 🎟 $12.50 per seat | Seats left: 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "### 🎬 **Today — September 25, 2025**\n", + "1. **Galactic Adventures** \n", + " *Science Fiction | PG-13 | 142 min* \n", + " ⏰ 14:30 — Theater A \n", + " 💺 105 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Epic space journey to save humanity from an alien threat.*\n", + "\n", + "2. **The Midnight Mystery** \n", + " *Thriller | R | 118 min* \n", + " ⏰ 19:15 — Theater B \n", + " 💺 75 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Small town sheriff investigates midnight disappearances.*\n", + "\n", + "3. **Laugh Out Loud** \n", + " *Comedy | PG-13 | 95 min* \n", + " ⏰ 21:45 — Theater A \n", + " 💺 60 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Three friends accidentally become viral internet stars.*\n", "\n", "---\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **Dragon's Heart** *(Animation, G)* \n", - " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", - " 🎟 $10.00 per seat | Seats left: 77/100 \n", - " *A young girl who can speak to dragons must save her village.* \n", - " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", - "\n", - "5. **City of Shadows** *(Drama, R)* \n", - " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", - " 🎟 $14.00 per seat | Seats left: 44/200 \n", - " *A noir mystery set in 1940s New York about corruption and murder.* \n", - " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", - "\n", - "6. **Ocean's Edge** *(Documentary, G)* \n", - " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", - " 🎟 $18.00 per seat | Seats left: 222/300 \n", - " *Exploring the ocean’s depths and its marvelous creatures.* \n", - " **Narrator:** David Attenborough\n", + "### 📅 **September 26, 2025**\n", + "4. **Dragon's Heart** \n", + " *Animation | G | 103 min* \n", + " ⏰ 10:00 — Theater C \n", + " 💺 77 / 100 seats available \n", + " 🎟 $10.00 per seat \n", + " *Young girl must save her village with help from dragons.*\n", + "\n", + "5. **City of Shadows** \n", + " *Drama | R | 134 min* \n", + " ⏰ 16:20 — Theater B \n", + " 💺 44 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Noir detective uncovers police corruption in 1940s NYC.*\n", + "\n", + "6. **Ocean's Edge** \n", + " *Documentary | G | 87 min* \n", + " ⏰ 20:00 — IMAX Theater \n", + " 💺 222 / 300 seats available \n", + " 🎟 $18.00 per seat \n", + " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", "\n", "---\n", "\n", "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** *(Romance, PG)* \n", - " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", - " 🎟 $11.50 per seat | Seats left: 31/100 \n", - " *An American tourist finds romance in Paris with a café owner.* \n", - " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", - "\n", - "8. **Nightmare Manor** *(Horror, R)* \n", - " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", - " 🎟 $13.00 per seat | Seats left: 58/150 \n", - " *A haunted mansion filled with the ghosts of past owners.* \n", - " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "7. **Love in Paris** \n", + " *Romance | PG | 108 min* \n", + " ⏰ 15:45 — Theater C \n", + " 💺 31 / 100 seats available \n", + " 🎟 $11.50 per seat \n", + " *American tourist finds love with a Paris café owner.*\n", + "\n", + "8. **Nightmare Manor** \n", + " *Horror | R | 106 min* \n", + " ⏰ 22:30 — Theater A \n", + " 💺 58 / 150 seats available \n", + " 🎟 $13.00 per seat \n", + " *Family inherits a haunted mansion with dark secrets.*\n", "\n", "---\n", "\n", - "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\n", + "Would you like me to show more details for any of these movies or help you make a reservation?\n", "\n", "==================================================\n", "\n", "\u001b[32;1m\u001b[1;3mHere’s what’s currently playing at **MovieMagic Cinema**:\n", "\n", - "---\n", - "\n", - "### 🎬 **Today – September 25, 2025**\n", - "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", - " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", - " 🎟 $12.50 per seat | Seats left: 105/150 \n", - " *An epic space journey to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **The Midnight Mystery** *(Thriller, R)* \n", - " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", - " 🎟 $14.00 per seat | Seats left: 75/200 \n", - " *A small-town sheriff unravels a string of midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", - " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", - " 🎟 $12.50 per seat | Seats left: 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "### 🎬 **Today — September 25, 2025**\n", + "1. **Galactic Adventures** \n", + " *Science Fiction | PG-13 | 142 min* \n", + " ⏰ 14:30 — Theater A \n", + " 💺 105 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Epic space journey to save humanity from an alien threat.*\n", + "\n", + "2. **The Midnight Mystery** \n", + " *Thriller | R | 118 min* \n", + " ⏰ 19:15 — Theater B \n", + " 💺 75 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Small town sheriff investigates midnight disappearances.*\n", + "\n", + "3. **Laugh Out Loud** \n", + " *Comedy | PG-13 | 95 min* \n", + " ⏰ 21:45 — Theater A \n", + " 💺 60 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Three friends accidentally become viral internet stars.*\n", "\n", "---\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **Dragon's Heart** *(Animation, G)* \n", - " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", - " 🎟 $10.00 per seat | Seats left: 77/100 \n", - " *A young girl who can speak to dragons must save her village.* \n", - " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", - "\n", - "5. **City of Shadows** *(Drama, R)* \n", - " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", - " 🎟 $14.00 per seat | Seats left: 44/200 \n", - " *A noir mystery set in 1940s New York about corruption and murder.* \n", - " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", - "\n", - "6. **Ocean's Edge** *(Documentary, G)* \n", - " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", - " 🎟 $18.00 per seat | Seats left: 222/300 \n", - " *Exploring the ocean’s depths and its marvelous creatures.* \n", - " **Narrator:** David Attenborough\n", + "### 📅 **September 26, 2025**\n", + "4. **Dragon's Heart** \n", + " *Animation | G | 103 min* \n", + " ⏰ 10:00 — Theater C \n", + " 💺 77 / 100 seats available \n", + " 🎟 $10.00 per seat \n", + " *Young girl must save her village with help from dragons.*\n", + "\n", + "5. **City of Shadows** \n", + " *Drama | R | 134 min* \n", + " ⏰ 16:20 — Theater B \n", + " 💺 44 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Noir detective uncovers police corruption in 1940s NYC.*\n", + "\n", + "6. **Ocean's Edge** \n", + " *Documentary | G | 87 min* \n", + " ⏰ 20:00 — IMAX Theater \n", + " 💺 222 / 300 seats available \n", + " 🎟 $18.00 per seat \n", + " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", "\n", "---\n", "\n", "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** *(Romance, PG)* \n", - " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", - " 🎟 $11.50 per seat | Seats left: 31/100 \n", - " *An American tourist finds romance in Paris with a café owner.* \n", - " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", - "\n", - "8. **Nightmare Manor** *(Horror, R)* \n", - " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", - " 🎟 $13.00 per seat | Seats left: 58/150 \n", - " *A haunted mansion filled with the ghosts of past owners.* \n", - " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "7. **Love in Paris** \n", + " *Romance | PG | 108 min* \n", + " ⏰ 15:45 — Theater C \n", + " 💺 31 / 100 seats available \n", + " 🎟 $11.50 per seat \n", + " *American tourist finds love with a Paris café owner.*\n", + "\n", + "8. **Nightmare Manor** \n", + " *Horror | R | 106 min* \n", + " ⏰ 22:30 — Theater A \n", + " 💺 58 / 150 seats available \n", + " 🎟 $13.00 per seat \n", + " *Family inherits a haunted mansion with dark secrets.*\n", "\n", "---\n", "\n", - "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\u001b[0m\n", + "Would you like me to show more details for any of these movies or help you make a reservation?\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n", "Here’s what’s currently playing at **MovieMagic Cinema**:\n", "\n", - "---\n", - "\n", - "### 🎬 **Today – September 25, 2025**\n", - "1. **Galactic Adventures** *(Science Fiction, PG-13)* \n", - " 🕒 14:30 | 📍 Theater A | ⏳ 142 min \n", - " 🎟 $12.50 per seat | Seats left: 105/150 \n", - " *An epic space journey to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **The Midnight Mystery** *(Thriller, R)* \n", - " 🕒 19:15 | 📍 Theater B | ⏳ 118 min \n", - " 🎟 $14.00 per seat | Seats left: 75/200 \n", - " *A small-town sheriff unravels a string of midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **Laugh Out Loud** *(Comedy, PG-13)* \n", - " 🕒 21:45 | 📍 Theater A | ⏳ 95 min \n", - " 🎟 $12.50 per seat | Seats left: 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "### 🎬 **Today — September 25, 2025**\n", + "1. **Galactic Adventures** \n", + " *Science Fiction | PG-13 | 142 min* \n", + " ⏰ 14:30 — Theater A \n", + " 💺 105 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Epic space journey to save humanity from an alien threat.*\n", + "\n", + "2. **The Midnight Mystery** \n", + " *Thriller | R | 118 min* \n", + " ⏰ 19:15 — Theater B \n", + " 💺 75 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Small town sheriff investigates midnight disappearances.*\n", + "\n", + "3. **Laugh Out Loud** \n", + " *Comedy | PG-13 | 95 min* \n", + " ⏰ 21:45 — Theater A \n", + " 💺 60 / 150 seats available \n", + " 🎟 $12.50 per seat \n", + " *Three friends accidentally become viral internet stars.*\n", "\n", "---\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **Dragon's Heart** *(Animation, G)* \n", - " 🕒 10:00 | 📍 Theater C | ⏳ 103 min \n", - " 🎟 $10.00 per seat | Seats left: 77/100 \n", - " *A young girl who can speak to dragons must save her village.* \n", - " **Voice Cast:** Amy Johnson, Mark Stevens, Luna Rodriguez\n", - "\n", - "5. **City of Shadows** *(Drama, R)* \n", - " 🕒 16:20 | 📍 Theater B | ⏳ 134 min \n", - " 🎟 $14.00 per seat | Seats left: 44/200 \n", - " *A noir mystery set in 1940s New York about corruption and murder.* \n", - " **Cast:** Antonio Silva, Catherine Moore, Frank Williams\n", - "\n", - "6. **Ocean's Edge** *(Documentary, G)* \n", - " 🕒 20:00 | 📍 IMAX Theater | ⏳ 87 min \n", - " 🎟 $18.00 per seat | Seats left: 222/300 \n", - " *Exploring the ocean’s depths and its marvelous creatures.* \n", - " **Narrator:** David Attenborough\n", + "### 📅 **September 26, 2025**\n", + "4. **Dragon's Heart** \n", + " *Animation | G | 103 min* \n", + " ⏰ 10:00 — Theater C \n", + " 💺 77 / 100 seats available \n", + " 🎟 $10.00 per seat \n", + " *Young girl must save her village with help from dragons.*\n", + "\n", + "5. **City of Shadows** \n", + " *Drama | R | 134 min* \n", + " ⏰ 16:20 — Theater B \n", + " 💺 44 / 200 seats available \n", + " 🎟 $14.00 per seat \n", + " *Noir detective uncovers police corruption in 1940s NYC.*\n", + "\n", + "6. **Ocean's Edge** \n", + " *Documentary | G | 87 min* \n", + " ⏰ 20:00 — IMAX Theater \n", + " 💺 222 / 300 seats available \n", + " 🎟 $18.00 per seat \n", + " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", "\n", "---\n", "\n", "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** *(Romance, PG)* \n", - " 🕒 15:45 | 📍 Theater C | ⏳ 108 min \n", - " 🎟 $11.50 per seat | Seats left: 31/100 \n", - " *An American tourist finds romance in Paris with a café owner.* \n", - " **Cast:** Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", - "\n", - "8. **Nightmare Manor** *(Horror, R)* \n", - " 🕒 22:30 | 📍 Theater A | ⏳ 106 min \n", - " 🎟 $13.00 per seat | Seats left: 58/150 \n", - " *A haunted mansion filled with the ghosts of past owners.* \n", - " **Cast:** Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "7. **Love in Paris** \n", + " *Romance | PG | 108 min* \n", + " ⏰ 15:45 — Theater C \n", + " 💺 31 / 100 seats available \n", + " 🎟 $11.50 per seat \n", + " *American tourist finds love with a Paris café owner.*\n", + "\n", + "8. **Nightmare Manor** \n", + " *Horror | R | 106 min* \n", + " ⏰ 22:30 — Theater A \n", + " 💺 58 / 150 seats available \n", + " 🎟 $13.00 per seat \n", + " *Family inherits a haunted mansion with dark secrets.*\n", "\n", "---\n", "\n", - "Would you like me to help you **pick a movie and reserve seats** for one of these showtimes?\n", + "Would you like me to show more details for any of these movies or help you make a reservation?\n", "\n", "==================================================\n", "\n" @@ -1182,7 +1206,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 51, "id": "9295d748", "metadata": {}, "outputs": [ @@ -1194,758 +1218,654 @@ "I can help you find movies, check showtimes, and make reservations.\n", "Type 'exit' to quit. Press Enter on an empty line to skip.\n", "\n", - "🎬 User: show me recent movies\n", + "🎬 User: Do I have any reservations?\n", "🤖 Assistant:\n", "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "🎬 User: show me recent movies\n", + "🎬 User: Do I have any reservations?\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mSure! Could you please provide the **email address** you used for your reservations so I can look them up?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Sure! Could you please provide the **email address** you used for your reservations so I can look them up?\n", + "\u001b[32;1m\u001b[1;3mSure! Could you please provide the **email address** you used for your reservations so I can look them up?\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Sure! Could you please provide the **email address** you used for your reservations so I can look them up?\n", + "🎬 User: kontakt@andrzejpytel.pl\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: kontakt@andrzejpytel.pl\n", "🤖 Assistant:\n", "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_current_movies` with `{}`\n", + "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", "\n", "\n", "\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_current_movies` with `{}`\n", + "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", "\n", "\n", "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"cinema_name\": \"MovieMagic Cinema\",\n", - " \"total_movies\": 8,\n", - " \"movies\": [\n", - " {\n", - " \"title\": \"Galactic Adventures\",\n", - " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"14:30\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 105,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 142,\n", - " \"genre\": \"Science Fiction\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Sarah Johnson\",\n", - " \"cast\": [\n", - " \"Alex Thompson\",\n", - " \"Maria Rodriguez\",\n", - " \"James Chen\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 30.0\n", - " },\n", - " {\n", - " \"title\": \"The Midnight Mystery\",\n", - " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"19:15\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 75,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 118,\n", - " \"genre\": \"Thriller\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Michael Davis\",\n", - " \"cast\": [\n", - " \"Emma Wilson\",\n", - " \"Robert Garcia\",\n", - " \"Lisa Park\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 62.5\n", - " },\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"total_reservations\": 3,\n", + " \"reservations\": [\n", " {\n", - " \"title\": \"Laugh Out Loud\",\n", - " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 61,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 95,\n", - " \"genre\": \"Comedy\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Jennifer Lee\",\n", - " \"cast\": [\n", - " \"Tom Martinez\",\n", - " \"Sarah Kim\",\n", - " \"David Brown\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 59.3\n", - " },\n", - " {\n", - " \"title\": \"Dragon's Heart\",\n", - " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"10:00\",\n", + " \"reservation_number\": 1,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 77,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 103,\n", - " \"genre\": \"Animation\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 10.0,\n", - " \"director\": \"Animation Studios Inc.\",\n", - " \"cast\": [\n", - " \"Voice Cast: Amy Johnson\",\n", - " \"Mark Stevens\",\n", - " \"Luna Rodriguez\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 23.0\n", - " },\n", - " {\n", - " \"title\": \"City of Shadows\",\n", - " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"16:20\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 44,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 134,\n", - " \"genre\": \"Drama\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Vincent Romano\",\n", - " \"cast\": [\n", - " \"Antonio Silva\",\n", - " \"Catherine Moore\",\n", - " \"Frank Williams\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 78.0\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", + " \"special_requests\": null\n", " },\n", " {\n", - " \"title\": \"Ocean's Edge\",\n", - " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"20:00\",\n", - " \"room\": \"IMAX Theater\",\n", - " \"seats_remaining\": 222,\n", - " \"seats_total\": 300,\n", - " \"duration_minutes\": 87,\n", - " \"genre\": \"Documentary\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 18.0,\n", - " \"director\": \"Ocean Explorer Films\",\n", - " \"cast\": [\n", - " \"Narrator: David Attenborough\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 26.0\n", - " },\n", - " {\n", - " \"title\": \"Love in Paris\",\n", - " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"reservation_number\": 2,\n", + " \"movie_title\": \"Love in Paris\",\n", " \"date\": \"2025-09-27\",\n", " \"time\": \"15:45\",\n", " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 31,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 108,\n", - " \"genre\": \"Romance\",\n", - " \"rating\": \"PG\",\n", - " \"price_per_seat\": 11.5,\n", - " \"director\": \"Claire Dubois\",\n", - " \"cast\": [\n", - " \"Sophie Martin\",\n", - " \"Jean-Luc Moreau\",\n", - " \"Isabella Jones\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 69.0\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", + " \"special_requests\": null\n", " },\n", " {\n", - " \"title\": \"Nightmare Manor\",\n", - " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"22:30\",\n", + " \"reservation_number\": 3,\n", + " \"movie_title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 58,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 106,\n", - " \"genre\": \"Horror\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 13.0,\n", - " \"director\": \"Horror Productions\",\n", - " \"cast\": [\n", - " \"Scary Actor 1\",\n", - " \"Scary Actor 2\",\n", - " \"Scary Actor 3\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 61.3\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", + " \"special_requests\": null\n", " }\n", " ]\n", "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"cinema_name\": \"MovieMagic Cinema\",\n", - " \"total_movies\": 8,\n", - " \"movies\": [\n", - " {\n", - " \"title\": \"Galactic Adventures\",\n", - " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"14:30\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 105,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 142,\n", - " \"genre\": \"Science Fiction\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Sarah Johnson\",\n", - " \"cast\": [\n", - " \"Alex Thompson\",\n", - " \"Maria Rodriguez\",\n", - " \"James Chen\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 30.0\n", - " },\n", - " {\n", - " \"title\": \"The Midnight Mystery\",\n", - " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"19:15\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 75,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 118,\n", - " \"genre\": \"Thriller\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Michael Davis\",\n", - " \"cast\": [\n", - " \"Emma Wilson\",\n", - " \"Robert Garcia\",\n", - " \"Lisa Park\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 62.5\n", - " },\n", - " {\n", - " \"title\": \"Laugh Out Loud\",\n", - " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 61,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 95,\n", - " \"genre\": \"Comedy\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Jennifer Lee\",\n", - " \"cast\": [\n", - " \"Tom Martinez\",\n", - " \"Sarah Kim\",\n", - " \"David Brown\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 59.3\n", - " },\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"total_reservations\": 3,\n", + " \"reservations\": [\n", " {\n", - " \"title\": \"Dragon's Heart\",\n", - " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"10:00\",\n", + " \"reservation_number\": 1,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 77,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 103,\n", - " \"genre\": \"Animation\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 10.0,\n", - " \"director\": \"Animation Studios Inc.\",\n", - " \"cast\": [\n", - " \"Voice Cast: Amy Johnson\",\n", - " \"Mark Stevens\",\n", - " \"Luna Rodriguez\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 23.0\n", - " },\n", - " {\n", - " \"title\": \"City of Shadows\",\n", - " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"16:20\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 44,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 134,\n", - " \"genre\": \"Drama\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Vincent Romano\",\n", - " \"cast\": [\n", - " \"Antonio Silva\",\n", - " \"Catherine Moore\",\n", - " \"Frank Williams\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 78.0\n", - " },\n", - " {\n", - " \"title\": \"Ocean's Edge\",\n", - " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"20:00\",\n", - " \"room\": \"IMAX Theater\",\n", - " \"seats_remaining\": 222,\n", - " \"seats_total\": 300,\n", - " \"duration_minutes\": 87,\n", - " \"genre\": \"Documentary\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 18.0,\n", - " \"director\": \"Ocean Explorer Films\",\n", - " \"cast\": [\n", - " \"Narrator: David Attenborough\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 26.0\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", + " \"special_requests\": null\n", " },\n", " {\n", - " \"title\": \"Love in Paris\",\n", - " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"reservation_number\": 2,\n", + " \"movie_title\": \"Love in Paris\",\n", " \"date\": \"2025-09-27\",\n", " \"time\": \"15:45\",\n", " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 31,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 108,\n", - " \"genre\": \"Romance\",\n", - " \"rating\": \"PG\",\n", - " \"price_per_seat\": 11.5,\n", - " \"director\": \"Claire Dubois\",\n", - " \"cast\": [\n", - " \"Sophie Martin\",\n", - " \"Jean-Luc Moreau\",\n", - " \"Isabella Jones\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 69.0\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", + " \"special_requests\": null\n", " },\n", " {\n", - " \"title\": \"Nightmare Manor\",\n", - " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"22:30\",\n", + " \"reservation_number\": 3,\n", + " \"movie_title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 58,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 106,\n", - " \"genre\": \"Horror\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 13.0,\n", - " \"director\": \"Horror Productions\",\n", - " \"cast\": [\n", - " \"Scary Actor 1\",\n", - " \"Scary Actor 2\",\n", - " \"Scary Actor 3\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 61.3\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", + " \"special_requests\": null\n", " }\n", " ]\n", - "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the **recent movies** currently playing at MovieMagic Cinema:\n", - "\n", - "---\n", - "\n", - "### 📅 **Today – September 25, 2025**\n", - "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", - " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 14:30 — **Seats left:** 105/150 \n", - " *Epic space adventure to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", - " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 19:15 — **Seats left:** 75/200 \n", - " *Small-town sheriff investigates mysterious midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", - " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 21:45 — **Seats left:** 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", - "\n", - "---\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are your current reservations:\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **🐉 Dragon's Heart** *(Animation, G)* \n", - " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", - " 🕒 10:00 — **Seats left:** 77/100 \n", - " *Girl who communicates with dragons must save her village.* \n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", "\n", - "5. **🏙 City of Shadows** *(Drama, R)* \n", - " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 16:20 — **Seats left:** 44/200 \n", - " *1940s noir mystery of murder and corruption.* \n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", "\n", - "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", - " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", - " 🕒 20:00 — **Seats left:** 222/300 \n", - " *Exploring the ocean’s depths narrated by David Attenborough.*\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **💖 Love in Paris** *(Romance, PG)* \n", - " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", - " 🕒 15:45 — **Seats left:** 31/100 \n", - " *American tourist finds love in Paris.* \n", - "\n", - "8. **👻 Nightmare Manor** *(Horror, R)* \n", - " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", - " 🕒 22:30 — **Seats left:** 58/150 \n", - " *A haunted mansion hides the spirits of its past owners.*\n", + "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", "\n", - "---\n", - "\n", - "Which one would you like me to get **more details on** or help you **book tickets** for?\u001b[0m\n", + "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are the **recent movies** currently playing at MovieMagic Cinema:\n", - "\n", - "---\n", - "\n", - "### 📅 **Today – September 25, 2025**\n", - "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", - " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 14:30 — **Seats left:** 105/150 \n", - " *Epic space adventure to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", - " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 19:15 — **Seats left:** 75/200 \n", - " *Small-town sheriff investigates mysterious midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", - " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 21:45 — **Seats left:** 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", - "\n", - "---\n", - "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **🐉 Dragon's Heart** *(Animation, G)* \n", - " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", - " 🕒 10:00 — **Seats left:** 77/100 \n", - " *Girl who communicates with dragons must save her village.* \n", - "\n", - "5. **🏙 City of Shadows** *(Drama, R)* \n", - " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 16:20 — **Seats left:** 44/200 \n", - " *1940s noir mystery of murder and corruption.* \n", - "\n", - "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", - " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", - " 🕒 20:00 — **Seats left:** 222/300 \n", - " *Exploring the ocean’s depths narrated by David Attenborough.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **💖 Love in Paris** *(Romance, PG)* \n", - " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", - " 🕒 15:45 — **Seats left:** 31/100 \n", - " *American tourist finds love in Paris.* \n", - "\n", - "8. **👻 Nightmare Manor** *(Horror, R)* \n", - " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", - " 🕒 22:30 — **Seats left:** 58/150 \n", - " *A haunted mansion hides the spirits of its past owners.*\n", + "Here are your current reservations:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", + "\n", + "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\n", + "\u001b[32;1m\u001b[1;3mHere are your current reservations:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", + "\n", + "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\u001b[0m\n", "\n", - "---\n", - "\n", - "Which one would you like me to get **more details on** or help you **book tickets** for?\n", - "\u001b[32;1m\u001b[1;3mHere are the **recent movies** currently playing at MovieMagic Cinema:\n", - "\n", - "---\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are your current reservations:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + "\n", + "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", + "\n", + "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\n", + "🎬 User: get my reservations please\n", + "🤖 Assistant:\n", "\n", - "### 📅 **Today – September 25, 2025**\n", - "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", - " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 14:30 — **Seats left:** 105/150 \n", - " *Epic space adventure to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", - " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 19:15 — **Seats left:** 75/200 \n", - " *Small-town sheriff investigates mysterious midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", - " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 21:45 — **Seats left:** 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", "\n", - "---\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: get my reservations please\n", + "🤖 Assistant:\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **🐉 Dragon's Heart** *(Animation, G)* \n", - " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", - " 🕒 10:00 — **Seats left:** 77/100 \n", - " *Girl who communicates with dragons must save her village.* \n", "\n", - "5. **🏙 City of Shadows** *(Drama, R)* \n", - " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 16:20 — **Seats left:** 44/200 \n", - " *1940s noir mystery of murder and corruption.* \n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", "\n", - "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", - " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", - " 🕒 20:00 — **Seats left:** 222/300 \n", - " *Exploring the ocean’s depths narrated by David Attenborough.*\n", "\n", - "---\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", "\n", - "### 📅 **September 27, 2025**\n", - "7. **💖 Love in Paris** *(Romance, PG)* \n", - " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", - " 🕒 15:45 — **Seats left:** 31/100 \n", - " *American tourist finds love in Paris.* \n", "\n", - "8. **👻 Nightmare Manor** *(Horror, R)* \n", - " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", - " 🕒 22:30 — **Seats left:** 58/150 \n", - " *A haunted mansion hides the spirits of its past owners.*\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"total_reservations\": 3,\n", + " \"reservations\": [\n", + " {\n", + " \"reservation_number\": 1,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 2,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 3,\n", + " \"movie_title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", + " \"special_requests\": null\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"total_reservations\": 3,\n", + " \"reservations\": [\n", + " {\n", + " \"reservation_number\": 1,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 2,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 3,\n", + " \"movie_title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", + " \"special_requests\": null\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are your current reservations: \n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", "\n", "---\n", "\n", - "Which one would you like me to get **more details on** or help you **book tickets** for?\u001b[0m\n", + "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", + "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are the **recent movies** currently playing at MovieMagic Cinema:\n", + "Here are your current reservations: \n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", "\n", "---\n", "\n", - "### 📅 **Today – September 25, 2025**\n", - "1. **🚀 Galactic Adventures** *(Sci-Fi, PG-13)* \n", - " ⏳ 142 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 14:30 — **Seats left:** 105/150 \n", - " *Epic space adventure to save humanity from alien threats.* \n", - " **Cast:** Alex Thompson, Maria Rodriguez, James Chen\n", - "\n", - "2. **🔎 The Midnight Mystery** *(Thriller, R)* \n", - " ⏳ 118 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 19:15 — **Seats left:** 75/200 \n", - " *Small-town sheriff investigates mysterious midnight disappearances.* \n", - " **Cast:** Emma Wilson, Robert Garcia, Lisa Park\n", - "\n", - "3. **😂 Laugh Out Loud** *(Comedy, PG-13)* \n", - " ⏳ 95 min | 🎟 $12.50 | 🎭 Theater A \n", - " 🕒 21:45 — **Seats left:** 61/150 \n", - " *Three friends accidentally become viral sensations.* \n", - " **Cast:** Tom Martinez, Sarah Kim, David Brown\n", + "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", + "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\n", + "\u001b[32;1m\u001b[1;3mHere are your current reservations: \n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", "\n", "---\n", "\n", - "### 📅 **Tomorrow – September 26, 2025**\n", - "4. **🐉 Dragon's Heart** *(Animation, G)* \n", - " ⏳ 103 min | 🎟 $10.00 | 🎭 Theater C \n", - " 🕒 10:00 — **Seats left:** 77/100 \n", - " *Girl who communicates with dragons must save her village.* \n", - "\n", - "5. **🏙 City of Shadows** *(Drama, R)* \n", - " ⏳ 134 min | 🎟 $14.00 | 🎭 Theater B \n", - " 🕒 16:20 — **Seats left:** 44/200 \n", - " *1940s noir mystery of murder and corruption.* \n", - "\n", - "6. **🌊 Ocean's Edge** *(Documentary, G, IMAX)* \n", - " ⏳ 87 min | 🎟 $18.00 | 🎭 IMAX Theater \n", - " 🕒 20:00 — **Seats left:** 222/300 \n", - " *Exploring the ocean’s depths narrated by David Attenborough.*\n", + "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", + "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\u001b[0m\n", "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **💖 Love in Paris** *(Romance, PG)* \n", - " ⏳ 108 min | 🎟 $11.50 | 🎭 Theater C \n", - " 🕒 15:45 — **Seats left:** 31/100 \n", - " *American tourist finds love in Paris.* \n", - "\n", - "8. **👻 Nightmare Manor** *(Horror, R)* \n", - " ⏳ 106 min | 🎟 $13.00 | 🎭 Theater A \n", - " 🕒 22:30 — **Seats left:** 58/150 \n", - " *A haunted mansion hides the spirits of its past owners.*\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are your current reservations: \n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", "\n", "---\n", "\n", - "Which one would you like me to get **more details on** or help you **book tickets** for?\n", - "🎬 User: I want to reserve one seat for Laugh Our Loud on 25th september, one seat, my name is Andrzej Pytel, contact email kontakt@andrzejpytel.pl\n", + "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", + "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\n", + "🎬 User: Check reservations for kontakt@andrzejpytel.pl\n", "🤖 Assistant:\n", "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "🎬 User: I want to reserve one seat for Laugh Our Loud on 25th september, one seat, my name is Andrzej Pytel, contact email kontakt@andrzejpytel.pl\n", + "🎬 User: Check reservations for kontakt@andrzejpytel.pl\n", "🤖 Assistant:\n", "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `make_reservation` with `{'title': 'Laugh Out Loud', 'date': '2025-09-25', 'time': '21:45', 'room': 'theater_a', 'seats_count': 1, 'customer_name': 'Andrzej Pytel', 'customer_email': 'kontakt@andrzejpytel.pl'}`\n", + "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", "\n", "\n", "\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `make_reservation` with `{'title': 'Laugh Out Loud', 'date': '2025-09-25', 'time': '21:45', 'room': 'theater_a', 'seats_count': 1, 'customer_name': 'Andrzej Pytel', 'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[31;1m\u001b[1;3m{\n", - " \"confirmation\": {\n", - " \"reservation_id\": 3,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_datetime\": \"2025-09-24T14:02:09.153700\"\n", - " },\n", - " \"movie_details\": {\n", - " \"title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"genre\": \"Comedy\",\n", - " \"duration_minutes\": 95\n", - " },\n", - " \"booking_details\": {\n", - " \"seats_reserved\": 1,\n", - " \"customer_name\": \"Andrzej Pytel\",\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"customer_phone\": null,\n", - " \"special_requests\": null\n", - " },\n", - " \"pricing\": {\n", - " \"price_per_seat\": 12.5,\n", - " \"total_price\": 12.5\n", - " }\n", - "}\u001b[0m\u001b[31;1m\u001b[1;3m{\n", - " \"confirmation\": {\n", - " \"reservation_id\": 3,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_datetime\": \"2025-09-24T14:02:09.153700\"\n", - " },\n", - " \"movie_details\": {\n", - " \"title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"genre\": \"Comedy\",\n", - " \"duration_minutes\": 95\n", - " },\n", - " \"booking_details\": {\n", - " \"seats_reserved\": 1,\n", - " \"customer_name\": \"Andrzej Pytel\",\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"customer_phone\": null,\n", - " \"special_requests\": null\n", - " },\n", - " \"pricing\": {\n", - " \"price_per_seat\": 12.5,\n", - " \"total_price\": 12.5\n", - " }\n", - "}\u001b[0m\u001b[32;1m\u001b[1;3m✅ **Reservation Confirmed!** \n", - "\n", - "Here are your booking details:\n", - "\n", - "---\n", + "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", "\n", - "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", - "📅 **Date:** September 25, 2025 \n", - "🕒 **Time:** 21:45 \n", - "📍 **Room:** Theater A \n", - "⏳ **Duration:** 95 minutes \n", "\n", - "👤 **Name:** Andrzej Pytel \n", - "📧 **Email:** kontakt@andrzejpytel.pl \n", - "🎟 **Seats Reserved:** 1 \n", - "💰 **Price Per Seat:** $12.50 \n", - "💵 **Total Price:** $12.50 \n", - "\n", - "---\n", - "\n", - "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", - "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\u001b[0m\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"total_reservations\": 3,\n", + " \"reservations\": [\n", + " {\n", + " \"reservation_number\": 1,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 2,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 3,\n", + " \"movie_title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", + " \"special_requests\": null\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", + " \"total_reservations\": 3,\n", + " \"reservations\": [\n", + " {\n", + " \"reservation_number\": 1,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 2,\n", + " \"movie_title\": \"Love in Paris\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", + " \"special_requests\": null\n", + " },\n", + " {\n", + " \"reservation_number\": 3,\n", + " \"movie_title\": \"Laugh Out Loud\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_reserved\": 1,\n", + " \"status\": \"confirmed\",\n", + " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", + " \"special_requests\": null\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the reservations for **kontakt@andrzejpytel.pl**:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", + "\n", + "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", + "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n", - "✅ **Reservation Confirmed!** \n", - "\n", - "Here are your booking details:\n", - "\n", - "---\n", - "\n", - "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", - "📅 **Date:** September 25, 2025 \n", - "🕒 **Time:** 21:45 \n", - "📍 **Room:** Theater A \n", - "⏳ **Duration:** 95 minutes \n", - "\n", - "👤 **Name:** Andrzej Pytel \n", - "📧 **Email:** kontakt@andrzejpytel.pl \n", - "🎟 **Seats Reserved:** 1 \n", - "💰 **Price Per Seat:** $12.50 \n", - "💵 **Total Price:** $12.50 \n", - "\n", - "---\n", - "\n", - "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", - "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\n", - "\u001b[32;1m\u001b[1;3m✅ **Reservation Confirmed!** \n", - "\n", - "Here are your booking details:\n", - "\n", - "---\n", - "\n", - "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", - "📅 **Date:** September 25, 2025 \n", - "🕒 **Time:** 21:45 \n", - "📍 **Room:** Theater A \n", - "⏳ **Duration:** 95 minutes \n", - "\n", - "👤 **Name:** Andrzej Pytel \n", - "📧 **Email:** kontakt@andrzejpytel.pl \n", - "🎟 **Seats Reserved:** 1 \n", - "💰 **Price Per Seat:** $12.50 \n", - "💵 **Total Price:** $12.50 \n", - "\n", - "---\n", - "\n", - "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", - "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\u001b[0m\n", + "Here are the reservations for **kontakt@andrzejpytel.pl**:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", + "\n", + "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", + "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\n", + "\u001b[32;1m\u001b[1;3mHere are the reservations for **kontakt@andrzejpytel.pl**:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", + "\n", + "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", + "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n", - "✅ **Reservation Confirmed!** \n", - "\n", - "Here are your booking details:\n", - "\n", - "---\n", - "\n", - "🎬 **Movie:** Laugh Out Loud *(Comedy)* \n", - "📅 **Date:** September 25, 2025 \n", - "🕒 **Time:** 21:45 \n", - "📍 **Room:** Theater A \n", - "⏳ **Duration:** 95 minutes \n", - "\n", - "👤 **Name:** Andrzej Pytel \n", - "📧 **Email:** kontakt@andrzejpytel.pl \n", - "🎟 **Seats Reserved:** 1 \n", - "💰 **Price Per Seat:** $12.50 \n", - "💵 **Total Price:** $12.50 \n", - "\n", - "---\n", - "\n", - "Your reservation ID is **#3** — please bring it or your confirmation email when you arrive. 🍿 \n", - "Would you like me to also send **directions to the cinema** or **add any snacks** to your booking?\n", + "Here are the reservations for **kontakt@andrzejpytel.pl**:\n", + "\n", + "1. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "2. **Love in Paris** \n", + " 📅 Date: September 27, 2025 \n", + " ⏰ Time: 15:45 \n", + " 🎭 Room: Theater C \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 13:55 \n", + "\n", + "3. **Laugh Out Loud** \n", + " 📅 Date: September 25, 2025 \n", + " ⏰ Time: 21:45 \n", + " 🎭 Room: Theater A \n", + " 🎟 Seats reserved: 1 \n", + " ✅ Status: Confirmed \n", + " 📌 Reserved on: September 24, 2025, 14:02 \n", + "\n", + "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", + "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\n", "🎬 Thanks for using MovieMagic Cinema! Goodbye!\n", "🎬 Thanks for using MovieMagic Cinema! Goodbye!\n" ] @@ -1986,7 +1906,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 52, "id": "e70dad2d", "metadata": {}, "outputs": [ diff --git a/src/mcp/cinema-mcp/cinema_service.py b/src/mcp/cinema-mcp/cinema_service.py index d4b6844..f340e4a 100644 --- a/src/mcp/cinema-mcp/cinema_service.py +++ b/src/mcp/cinema-mcp/cinema_service.py @@ -32,7 +32,9 @@ def get_current_movies_data() -> Dict[str, Any]: movies_list = [] for movie_data in current_movies: movie = parse_movie_data(movie_data) - movies_list.append({ + # Only show movies with available seats + if movie.seats_remaining > 0: + movies_list.append({ "title": movie.title, "description": movie.description, "date": movie.date.isoformat(), @@ -48,7 +50,7 @@ def get_current_movies_data() -> Dict[str, Any]: "cast": movie_data.get("cast", []), "is_sold_out": movie.is_sold_out, "occupancy_percentage": round(movie.occupancy_percentage, 1) - }) + }) return { "cinema_name": "MovieMagic Cinema", @@ -437,7 +439,7 @@ def get_customer_reservations_data(customer_email: str) -> Dict[str, Any]: email = customer_email.strip().lower() customer_reservations = [ res for res in RESERVATIONS - if res.customer_email.lower() == email + if res.customer_email.lower() == email and res.status != "cancelled" ] if not customer_reservations: @@ -540,4 +542,210 @@ def cancel_reservation_data( } except Exception as e: - return {"error": f"Failed to cancel reservation: {str(e)}"} \ No newline at end of file + return {"error": f"Failed to cancel reservation: {str(e)}"} + + +# Employee Management Functions +def get_all_reservations_data(status_filter: Optional[str] = None) -> Dict[str, Any]: + """Get all reservations for cinema employee review + + Args: + status_filter: Filter by status ("confirmed", "completed", "cancelled") or None for all + + Returns: + List of all reservations matching the filter + """ + try: + # Filter reservations by status if specified + if status_filter: + filtered_reservations = [ + res for res in RESERVATIONS + if res.status.lower() == status_filter.lower() + ] + else: + filtered_reservations = RESERVATIONS.copy() + + if not filtered_reservations: + status_msg = f" with status '{status_filter}'" if status_filter else "" + return {"error": f"No reservations found{status_msg}"} + + reservations_list = [] + for i, reservation in enumerate(filtered_reservations, 1): + room_name = CINEMA_ROOMS.get(reservation.room, {}).get("name", reservation.room) + + reservations_list.append({ + "reservation_number": i, + "movie_title": reservation.movie_title, + "date": reservation.movie_date.isoformat(), + "time": reservation.movie_time.strftime("%H:%M"), + "room": room_name, + "seats_reserved": reservation.seats_reserved, + "customer_name": reservation.customer_name, + "customer_email": reservation.customer_email, + "customer_phone": reservation.contact_info.phone, + "status": reservation.status, + "reservation_made": reservation.reservation_datetime.isoformat(), + "special_requests": reservation.special_requests + }) + + return { + "status_filter": status_filter or "all", + "total_reservations": len(reservations_list), + "reservations": reservations_list + } + + except Exception as e: + return {"error": f"Failed to get reservations: {str(e)}"} + + +def acknowledge_reservation_data( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Acknowledge/complete a reservation (employee function) + + Args: + customer_email: Customer's email address + title: Movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Acknowledgment confirmation or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + + # Parse date and time for comparison + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + target_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Find the reservation + reservation_to_acknowledge = None + for reservation in RESERVATIONS: + if (reservation.customer_email.lower() == email and + reservation.movie_title.lower() == title.lower() and + reservation.movie_date == target_date and + reservation.movie_time == target_time and + reservation.room.lower() == room.lower() and + reservation.status == "confirmed"): + reservation_to_acknowledge = reservation + break + + if not reservation_to_acknowledge: + return {"error": f"No confirmed reservation found for {customer_email} for '{title}' on {date} at {time} in {room}"} + + # Acknowledge the reservation (mark as completed) + reservation_to_acknowledge.status = "completed" + + # Since the customer attended, we don't free up seats - they were actually used + # The seats remain "booked" as they were occupied during the showing + + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + + return { + "acknowledgment_confirmed": True, + "completed_reservation": { + "movie_title": title, + "date": date, + "time": time, + "room": room_name, + "seats_used": reservation_to_acknowledge.seats_reserved, + "customer_email": customer_email, + "customer_name": reservation_to_acknowledge.customer_name + }, + "message": f"Reservation acknowledged as completed. Customer {reservation_to_acknowledge.customer_name} attended the showing." + } + + except Exception as e: + return {"error": f"Failed to acknowledge reservation: {str(e)}"} + + +def revoke_reservation_data( + customer_email: str, + title: str, + date: str, + time: str, + room: str, + reason: Optional[str] = None +) -> Dict[str, Any]: + """Revoke/cancel a reservation (employee function) + + Args: + customer_email: Customer's email address + title: Movie title + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + reason: Reason for revocation (optional) + + Returns: + Revocation confirmation or error dict + """ + try: + if not customer_email or not customer_email.strip(): + return {"error": "Customer email is required"} + + email = customer_email.strip().lower() + + # Parse date and time for comparison + try: + target_date = datetime.strptime(date, "%Y-%m-%d").date() + target_time = datetime.strptime(time, "%H:%M").time() + except ValueError as e: + return {"error": f"Invalid date or time format: {str(e)}"} + + # Find the reservation + reservation_to_revoke = None + for reservation in RESERVATIONS: + if (reservation.customer_email.lower() == email and + reservation.movie_title.lower() == title.lower() and + reservation.movie_date == target_date and + reservation.movie_time == target_time and + reservation.room.lower() == room.lower() and + reservation.status == "confirmed"): + reservation_to_revoke = reservation + break + + if not reservation_to_revoke: + return {"error": f"No confirmed reservation found for {customer_email} for '{title}' on {date} at {time} in {room}"} + + # Revoke the reservation (mark as cancelled) + reservation_to_revoke.status = "cancelled" + if reason: + reservation_to_revoke.special_requests = f"CANCELLED: {reason}" + + # Free up the seats (same as customer cancellation) + movie_data = get_movie_by_natural_id(title, date, time, room) + if movie_data: + movie_data["seats_booked"] = max(0, movie_data.get("seats_booked", 0) - reservation_to_revoke.seats_reserved) + + room_name = CINEMA_ROOMS.get(room, {}).get("name", room) + + return { + "revocation_confirmed": True, + "revoked_reservation": { + "movie_title": title, + "date": date, + "time": time, + "room": room_name, + "seats_freed": reservation_to_revoke.seats_reserved, + "customer_email": customer_email, + "customer_name": reservation_to_revoke.customer_name, + "reason": reason + }, + "message": f"Reservation revoked successfully. {reservation_to_revoke.seats_reserved} seats have been freed up. Reason: {reason or 'No reason provided'}" + } + + except Exception as e: + return {"error": f"Failed to revoke reservation: {str(e)}"} \ No newline at end of file diff --git a/src/mcp/cinema-mcp/main.py b/src/mcp/cinema-mcp/main.py index 41f316e..2aa7e3f 100644 --- a/src/mcp/cinema-mcp/main.py +++ b/src/mcp/cinema-mcp/main.py @@ -17,7 +17,10 @@ search_movies_data, create_reservation_data, get_customer_reservations_data, - cancel_reservation_data + cancel_reservation_data, + get_all_reservations_data, + acknowledge_reservation_data, + revoke_reservation_data ) mcp = FastMCP("Cinema", port=8010) @@ -151,6 +154,67 @@ def cancel_reservation( """ return cancel_reservation_data(customer_email, title, date, time, room) +@mcp.tool() +def get_all_reservations(status_filter: Optional[str] = None) -> Dict[str, Any]: + """Get all reservations for cinema employee review + + Args: + status_filter: Filter by status ("confirmed", "completed", "cancelled") or None for all active reservations + + Returns: + List of all reservations matching the filter for employee management + Use this to review current reservations that need attention + """ + return get_all_reservations_data(status_filter) + +@mcp.tool() +def acknowledge_reservation( + customer_email: str, + title: str, + date: str, + time: str, + room: str +) -> Dict[str, Any]: + """Acknowledge/complete a reservation (cinema employee function) + + Args: + customer_email: Customer's email address + title: Exact movie title of the reservation + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + + Returns: + Acknowledgment confirmation - marks reservation as completed + Use when customer has attended the showing + """ + return acknowledge_reservation_data(customer_email, title, date, time, room) + +@mcp.tool() +def revoke_reservation( + customer_email: str, + title: str, + date: str, + time: str, + room: str, + reason: Optional[str] = None +) -> Dict[str, Any]: + """Revoke/cancel a reservation (cinema employee function) + + Args: + customer_email: Customer's email address + title: Exact movie title of the reservation to revoke + date: Date in YYYY-MM-DD format + time: Time in HH:MM format + room: Room identifier + reason: Reason for revocation (optional, for employee records) + + Returns: + Revocation confirmation and details about freed seats + Use when a reservation needs to be cancelled by cinema staff + """ + return revoke_reservation_data(customer_email, title, date, time, room, reason) + # Resources @mcp.resource("movie://{title}/{date}/{time}/{room}") def get_movie_resource(title: str, date: str, time: str, room: str) -> str: From 0c58ab59e245750aa185bcf4d6e6d729a5c238e3 Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 14:32:48 +0100 Subject: [PATCH 16/25] Add reviews functions to services and trying to fix stuff --- src/mcp/movie-reviews-mcp/config.py | 135 ++++++++++++++------- src/mcp/movie-reviews-mcp/main.py | 65 +++++----- src/mcp/movie-reviews-mcp/models.py | 13 +- src/mcp/movie-reviews-mcp/movie_service.py | 74 ++++++++++- 4 files changed, 207 insertions(+), 80 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/config.py b/src/mcp/movie-reviews-mcp/config.py index 0aa58a6..7cc1486 100644 --- a/src/mcp/movie-reviews-mcp/config.py +++ b/src/mcp/movie-reviews-mcp/config.py @@ -189,270 +189,315 @@ "movie_id": 1, "reviewer": "Alice", "rating": 5, - "comment": "A mind-bending masterpiece with stunning visuals and a gripping plot." + "comment": "A mind-bending masterpiece with stunning visuals and a gripping plot.", + "reviewDate": "2024-03-01" }, { "movie_id": 1, "reviewer": "Bob", "rating": 4, - "comment": "Complex and thought-provoking, but a bit hard to follow at times." + "comment": "Complex and thought-provoking, but a bit hard to follow at times.", + "reviewDate": "2024-03-02" }, { "movie_id": 2, "reviewer": "Charlie", "rating": 5, - "comment": "An epic tale of family and power. A must-watch classic." + "comment": "An epic tale of family and power. A must-watch classic.", + "reviewDate": "2024-03-03" }, { "movie_id": 2, "reviewer": "Diana", "rating": 5, - "comment": "Brilliant performances and an unforgettable story." + "comment": "Brilliant performances and an unforgettable story.", + "reviewDate": "2024-03-04" }, { "movie_id": 3, "reviewer": "Eve", "rating": 5, - "comment": "Heath Ledger's Joker is iconic. A thrilling ride from start to finish." + "comment": "Heath Ledger's Joker is iconic. A thrilling ride from start to finish.", + "reviewDate": "2024-03-05" }, { "movie_id": 3, "reviewer": "Frank", "rating": 4, - "comment": "Great action and depth, though a bit dark for my taste." + "comment": "Great action and depth, though a bit dark for my taste.", + "reviewDate": "2024-03-06" }, { "movie_id": 4, "reviewer": "Grace", "rating": 5, - "comment": "A wild, stylish film with unforgettable dialogue." + "comment": "A wild, stylish film with unforgettable dialogue.", + "reviewDate": "2024-03-07" }, { "movie_id": 4, "reviewer": "Henry", "rating": 4, - "comment": "Unique storytelling and great cast." + "comment": "Unique storytelling and great cast.", + "reviewDate": "2024-03-08" }, { "movie_id": 5, "reviewer": "Ivy", "rating": 5, - "comment": "Heartwarming and inspirational. Tom Hanks is fantastic." + "comment": "Heartwarming and inspirational. Tom Hanks is fantastic.", + "reviewDate": "2024-03-09" }, { "movie_id": 5, "reviewer": "Jack", "rating": 4, - "comment": "A touching story with memorable moments." + "comment": "A touching story with memorable moments.", + "reviewDate": "2024-03-10" }, { "movie_id": 6, "reviewer": "Karen", "rating": 5, - "comment": "Visually stunning and emotionally powerful." + "comment": "Visually stunning and emotionally powerful.", + "reviewDate": "2024-03-11" }, { "movie_id": 6, "reviewer": "Leo", "rating": 4, - "comment": "Ambitious sci-fi with a moving story." + "comment": "Ambitious sci-fi with a moving story.", + "reviewDate": "2024-03-12" }, { "movie_id": 7, "reviewer": "Mona", "rating": 5, - "comment": "A brilliant social satire with suspenseful twists." + "comment": "A brilliant social satire with suspenseful twists.", + "reviewDate": "2024-03-13" }, { "movie_id": 7, "reviewer": "Nate", "rating": 4, - "comment": "Darkly funny and deeply unsettling." + "comment": "Darkly funny and deeply unsettling.", + "reviewDate": "2024-03-14" }, { "movie_id": 8, "reviewer": "Olivia", "rating": 5, - "comment": "A powerful and emotional historical drama." + "comment": "A powerful and emotional historical drama.", + "reviewDate": "2024-03-15" }, { "movie_id": 8, "reviewer": "Paul", "rating": 5, - "comment": "Heart-wrenching and beautifully acted." + "comment": "Heart-wrenching and beautifully acted.", + "reviewDate": "2024-03-16" }, { "movie_id": 9, "reviewer": "Quinn", "rating": 5, - "comment": "A moving story of hope and friendship." + "comment": "A moving story of hope and friendship.", + "reviewDate": "2024-03-17" }, { "movie_id": 9, "reviewer": "Rita", "rating": 5, - "comment": "Timeless and uplifting. A true classic." + "comment": "Timeless and uplifting. A true classic.", + "reviewDate": "2024-03-18" }, { "movie_id": 10, "reviewer": "Sam", "rating": 5, - "comment": "Magical animation with a rich, imaginative world." + "comment": "Magical animation with a rich, imaginative world.", + "reviewDate": "2024-03-19" }, { "movie_id": 10, "reviewer": "Tina", "rating": 4, - "comment": "Beautiful visuals and a touching story." + "comment": "Beautiful visuals and a touching story.", + "reviewDate": "2024-03-20" }, { "movie_id": 11, "reviewer": "Uma", "rating": 5, - "comment": "Epic battles and a compelling hero." + "comment": "Epic battles and a compelling hero.", + "reviewDate": "2024-03-21" }, { "movie_id": 11, "reviewer": "Victor", "rating": 4, - "comment": "Intense action and strong performances." + "comment": "Intense action and strong performances.", + "reviewDate": "2024-03-22" }, { "movie_id": 12, "reviewer": "Wendy", "rating": 5, - "comment": "Mind-blowing concept and great action." + "comment": "Mind-blowing concept and great action.", + "reviewDate": "2024-03-23" }, { "movie_id": 12, "reviewer": "Xander", "rating": 4, - "comment": "Innovative and entertaining sci-fi." + "comment": "Innovative and entertaining sci-fi.", + "reviewDate": "2024-03-24" }, { "movie_id": 13, "reviewer": "Yara", "rating": 5, - "comment": "Dark, clever, and unforgettable." + "comment": "Dark, clever, and unforgettable.", + "reviewDate": "2024-03-25" }, { "movie_id": 13, "reviewer": "Zane", "rating": 4, - "comment": "Unique story with strong performances." + "comment": "Unique story with strong performances.", + "reviewDate": "2024-03-26" }, { "movie_id": 14, "reviewer": "Abby", "rating": 5, - "comment": "Epic fantasy with breathtaking visuals." + "comment": "Epic fantasy with breathtaking visuals.", + "reviewDate": "2024-03-27" }, { "movie_id": 14, "reviewer": "Ben", "rating": 5, - "comment": "A fitting end to a legendary trilogy." + "comment": "A fitting end to a legendary trilogy.", + "reviewDate": "2024-03-28" }, { "movie_id": 15, "reviewer": "Cara", "rating": 5, - "comment": "A childhood favorite with memorable songs." + "comment": "A childhood favorite with memorable songs.", + "reviewDate": "2024-03-29" }, { "movie_id": 15, "reviewer": "Dan", "rating": 4, - "comment": "Beautiful animation and a touching story." + "comment": "Beautiful animation and a touching story.", + "reviewDate": "2024-03-30" }, { "movie_id": 16, "reviewer": "Ella", "rating": 5, - "comment": "Gripping crime drama with stellar acting." + "comment": "Gripping crime drama with stellar acting.", + "reviewDate": "2024-03-31" }, { "movie_id": 16, "reviewer": "Finn", "rating": 4, - "comment": "Intense and well-crafted mob story." + "comment": "Intense and well-crafted mob story.", + "reviewDate": "2024-04-01" }, { "movie_id": 17, "reviewer": "Gina", "rating": 5, - "comment": "Chilling and suspenseful thriller." + "comment": "Chilling and suspenseful thriller.", + "reviewDate": "2024-04-02" }, { "movie_id": 17, "reviewer": "Hank", "rating": 4, - "comment": "Great performances and a gripping plot." + "comment": "Great performances and a gripping plot.", + "reviewDate": "2024-04-03" }, { "movie_id": 18, "reviewer": "Iris", "rating": 5, - "comment": "A powerful and realistic war film." + "comment": "A powerful and realistic war film.", + "reviewDate": "2024-04-04" }, { "movie_id": 18, "reviewer": "Jake", "rating": 4, - "comment": "Intense and emotional storytelling." + "comment": "Intense and emotional storytelling.", + "reviewDate": "2024-04-05" }, { "movie_id": 19, "reviewer": "Kara", "rating": 5, - "comment": "A fascinating tale of rivalry and obsession." + "comment": "A fascinating tale of rivalry and obsession.", + "reviewDate": "2024-04-06" }, { "movie_id": 19, "reviewer": "Liam", "rating": 4, - "comment": "Intriguing plot with excellent twists." + "comment": "Intriguing plot with excellent twists.", + "reviewDate": "2024-04-07" }, { "movie_id": 20, "reviewer": "Mia", "rating": 5, - "comment": "Electrifying performances and a compelling story." + "comment": "Electrifying performances and a compelling story.", + "reviewDate": "2024-04-08" }, { "movie_id": 20, "reviewer": "Noah", "rating": 4, - "comment": "Intense and inspiring musical drama." + "comment": "Intense and inspiring musical drama.", + "reviewDate": "2024-04-09" }, { "movie_id": 1, "reviewer": "Oscar", "rating": 2, - "comment": "Too confusing and dragged on for too long." + "comment": "Too confusing and dragged on for too long.", + "reviewDate": "2024-04-10" }, { "movie_id": 5, "reviewer": "Pam", "rating": 2, - "comment": "Overrated and slow, didn't connect with the story." + "comment": "Overrated and slow, didn't connect with the story.", + "reviewDate": "2024-04-11" }, { "movie_id": 7, "reviewer": "Quentin", "rating": 1, - "comment": "Found it boring and hard to follow." + "comment": "Found it boring and hard to follow.", + "reviewDate": "2024-04-12" }, { "movie_id": 12, "reviewer": "Ralph", "rating": 2, - "comment": "Not as groundbreaking as people say, felt dated." + "comment": "Not as groundbreaking as people say, felt dated.", + "reviewDate": "2024-04-13" }, { "movie_id": 15, "reviewer": "Sophie", "rating": 1, - "comment": "Didn't enjoy the animation style or the music." + "comment": "Didn't enjoy the animation style or the music.", + "reviewDate": "2024-04-14" } ] \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py index 1b20710..a08b26c 100644 --- a/src/mcp/movie-reviews-mcp/main.py +++ b/src/mcp/movie-reviews-mcp/main.py @@ -16,11 +16,11 @@ get_random_movie_data, parse_movie_data, search_movies_data, - get_random_famous_movie_data, get_movie_details_data, get_movie_reviews_data, format_movie_resource, - format_search_results + format_search_results, + format_review_list ) mcp = FastMCP("MovieReviews", port=8011) @@ -94,6 +94,11 @@ def get_movie_resource(movie_id: int) -> str: """Get movie information as a formatted resource""" return format_movie_resource(movie_id) +@mcp.resource("movie://review/{movie_id}") +def get_movie_reviews_resource(movie_id: int) -> str: + """Get movie information as a formatted resource""" + return format_movie_resource(movie_id) + @mcp.resource("movies://genre/{genre}") def get_movies_by_genre_resource(genre: str) -> str: """Get movies by genre as a formatted resource""" @@ -114,34 +119,34 @@ def get_movie_genres_resource() -> str: # prompts -@mcp.prompt() -def travel_planning_prompt(location: str, days: int = 3) -> str: - """Generate a prompt for travel planning with attractions""" - return f"""Please help plan a {days}-day itinerary for {location}, including: -1. Top must-see attractions and landmarks -2. Best time to visit each attraction -3. Recommended booking strategies and timing -4. Transportation between attractions -5. Estimated costs and budgeting tips -6. Cultural considerations and local customs -7. Alternative attractions if main ones are crowded - -Focus on creating a balanced mix of historical, cultural, and recreational activities suitable for different interests.""" - -@mcp.prompt() -def attraction_comparison_prompt(attraction_ids: str) -> str: - """Generate a prompt for comparing multiple attractions""" - return f"""Please provide a detailed comparison of these attractions (IDs: {attraction_ids}), including: -1. Unique features and highlights of each -2. Best times to visit and crowd levels -3. Entry requirements and booking procedures -4. Approximate visit duration -5. Nearby attractions and activities -6. Accessibility and facilities -7. Value for money assessment -8. Personal recommendations based on different travel styles - -Help decide which attractions to prioritize based on time, budget, and interests.""" +# @mcp.prompt() +# def travel_planning_prompt(location: str, days: int = 3) -> str: +# """Generate a prompt for travel planning with attractions""" +# return f"""Please help plan a {days}-day itinerary for {location}, including: +# 1. Top must-see attractions and landmarks +# 2. Best time to visit each attraction +# 3. Recommended booking strategies and timing +# 4. Transportation between attractions +# 5. Estimated costs and budgeting tips +# 6. Cultural considerations and local customs +# 7. Alternative attractions if main ones are crowded + +# Focus on creating a balanced mix of historical, cultural, and recreational activities suitable for different interests.""" + +# @mcp.prompt() +# def attraction_comparison_prompt(attraction_ids: str) -> str: +# """Generate a prompt for comparing multiple attractions""" +# return f"""Please provide a detailed comparison of these attractions (IDs: {attraction_ids}), including: +# 1. Unique features and highlights of each +# 2. Best times to visit and crowd levels +# 3. Entry requirements and booking procedures +# 4. Approximate visit duration +# 5. Nearby attractions and activities +# 6. Accessibility and facilities +# 7. Value for money assessment +# 8. Personal recommendations based on different travel styles + +# Help decide which attractions to prioritize based on time, budget, and interests.""" if __name__ == "__main__": mcp.run(transport="streamable-http") \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/models.py b/src/mcp/movie-reviews-mcp/models.py index 0df5baa..4ce56c7 100644 --- a/src/mcp/movie-reviews-mcp/models.py +++ b/src/mcp/movie-reviews-mcp/models.py @@ -9,9 +9,10 @@ # movie reviews @dataclass class MovieReview: - movieId: int + movie_id: int rating: int - review: str + comment: str + reviewer: str reviewDate: datetime # movie @@ -24,10 +25,16 @@ class Movie: durationMins: int year: int genres: List[str] - reviews: List[MovieReview] @dataclass class MoviesList: genre: str total_count: int = 0 movies: List[Movie] = None + +@dataclass +class MovieReviewList: + movie: Movie + reviews: List[MovieReview] + total_count: int = 0 + avg_rating: Optional[float] = None \ No newline at end of file diff --git a/src/mcp/movie-reviews-mcp/movie_service.py b/src/mcp/movie-reviews-mcp/movie_service.py index 554be28..42c633f 100644 --- a/src/mcp/movie-reviews-mcp/movie_service.py +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -6,12 +6,15 @@ from dataclasses import asdict from datetime import datetime -from config import DEFAULT_SEARCH_LIMIT, MOVIE_GENRES +from config import DEFAULT_SEARCH_LIMIT, MOVIE_GENRES, MOCK_REVIEWS + from models import ( - Movie, MovieReview, MoviesList + Movie, MovieReview, MovieReviewList, MoviesList ) from utils import ( + format_movie_details, get_genre_display_name, + get_movie_by_id, parse_movie_data, search_movies, get_random_famous_movie @@ -98,6 +101,36 @@ def get_movie_genres_data() -> str: except Exception as e: return f"Error: Failed to get categories: {str(e)}" +def get_movie_details_data(movie_id: int): + """Get details for a specific movie + + Args: + movie_id: ID of the movie + Returns: + Movie object as dictionary or error dict + """ + try: + movie_data = get_movie_by_id(movie_id) + if not movie_data: + return {"error": f"Movie with ID {movie_id} not found"} + + movie = parse_movie_data(asdict(movie_data)) + return {"movie": asdict(movie)} + + except Exception as e: + return {"error": f"Failed to get movie details: {str(e)}"} + +def format_movie_resource(movie_id: int) -> str: + """Get movie information as a formatted resource""" + data = get_movie_details_data(movie_id) + if "error" in data: + return f"Error: {data['error']}" + + movie_data = data['movie'] + movie = parse_movie_data(movie_data) + + return format_movie_details(movie) + def format_search_results(search_data: Dict[str, Any]) -> str: """Format search results as a readable string""" @@ -131,3 +164,40 @@ def format_search_results(search_data: Dict[str, Any]) -> str: return result +def get_movie_reviews_data(movie_id: int) -> MovieReviewList: + """Get reviews for a specific movie + + Args: + movie_id: ID of the movie + Returns: + MovieReviewList object as dictionary or error dict + """ + + movie = get_movie_by_id(movie_id) + + reviews = [review for review in MOCK_REVIEWS if review.movie_id == movie_id] + review_list = MovieReviewList( + movie=movie, + total_count=len(reviews), + reviews=reviews, + avg_rating=(sum(r.rating for r in reviews) / len(reviews)) if reviews else None + ) + return review_list + +def format_review_list(review_list: MovieReviewList) -> str: + """Format movie review list as a readable string""" + if not review_list.reviews: + return f"No reviews found for movie: {review_list.movie.title}" + + result = f"📝 Reviews for **{review_list.movie.title}**\n" + result += f"Total Reviews: {review_list.total_count}\n" + if review_list.avg_rating: + result += f"Average Rating: {review_list.avg_rating:.1f}/5.0\n" + result += "\n" + + for i, review in enumerate(review_list.reviews, 1): + review_date = review.reviewDate.strftime("%Y-%m-%d") + result += f"{i}. ⭐ {review.rating}/5.0 on {review_date} by {review.reviewer}\n" + result += f" \"{review.comment}\"\n\n" + + return result.strip() \ No newline at end of file From e74f1affdde786d0e7ce259669258bc25759ed8c Mon Sep 17 00:00:00 2001 From: Henry Ing-Simmons Date: Wed, 24 Sep 2025 14:46:07 +0100 Subject: [PATCH 17/25] Update main.py --- src/mcp/movie-reviews-mcp/main.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py index a08b26c..d5ce01d 100644 --- a/src/mcp/movie-reviews-mcp/main.py +++ b/src/mcp/movie-reviews-mcp/main.py @@ -88,17 +88,32 @@ def search_and_format_movies( search_data = search_movies_data(genre, limit) return format_search_results(search_data) + +@mcp.tool() +def get_movie_reviews_by_title(title: string) -> str: + """Get reviews for a movie by its title and return formatted results + + Args: + title: Title of the movie + + Returns: + Formatted string with movie reviews + """ + # First, search for the movie by title to get its ID + search_data = search_movies(title=title, limit=1) + if not search_data or "movies" not in search_data or not search_data["movies"]: + return f"No movie found with title '{title}'." + + movie_id = search_data["movies"][0]["id"] + reviews_data = get_movie_reviews_data(movie_id) + return format_review_list(reviews_data) + # resources @mcp.resource("movie://{movie_id}") def get_movie_resource(movie_id: int) -> str: """Get movie information as a formatted resource""" return format_movie_resource(movie_id) -@mcp.resource("movie://review/{movie_id}") -def get_movie_reviews_resource(movie_id: int) -> str: - """Get movie information as a formatted resource""" - return format_movie_resource(movie_id) - @mcp.resource("movies://genre/{genre}") def get_movies_by_genre_resource(genre: str) -> str: """Get movies by genre as a formatted resource""" From 89ba09f8c9ab7e68cc7afc5eb04ad712e4ce8ed6 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:55:20 +0100 Subject: [PATCH 18/25] Changes --- demo/demo_template 1.pptx | Bin 2153430 -> 2154044 bytes src/agent/attractions.ipynb | 2 +- src/agent/cinema.ipynb | 1714 +++++------------------------ src/mcp/movie-reviews-mcp/uv.lock | 32 +- 4 files changed, 282 insertions(+), 1466 deletions(-) diff --git a/demo/demo_template 1.pptx b/demo/demo_template 1.pptx index 1732a44fbb859134bb92e70e6d14f0d741ae3d9a..20f841d62707894e64a700f4e545ca2fda3354b4 100644 GIT binary patch delta 7757 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 delta 7122 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 Azure AI Foundry Portal\n", @@ -196,15 +201,17 @@ " tools = await create_mcp_tools()\n", " \n", " if not tools:\n", - " print(\"No MCP tools loaded. Make sure the Cinema MCP server is accessible.\")\n", + " print(\"No MCP tools loaded. Make sure the Cinema and Movie Reviews MCP servers are accessible.\")\n", " return None\n", " \n", " print(f\"Loaded {len(tools)} MCP tools: {[tool.name for tool in tools]}\")\n", " \n", - " # Create system prompt for cinema assistant\n", - " system_prompt = \"\"\"You are a helpful cinema assistant that can help users discover movies and make reservations.\n", + " # Create system prompt for cinema and movie reviews assistant\n", + " system_prompt = \"\"\"You are a helpful movie and cinema assistant that can help users discover movies, get reviews, and make reservations.\n", + " \n", + " You have access to both cinema and movie reviews MCP tools including:\n", " \n", - " You have access to cinema MCP tools for movie showings, including:\n", + " Cinema tools:\n", " - Getting current movies playing with showtimes and availability\n", " - Searching for movies by title, genre, date, room, and seat availability\n", " - Getting detailed information about specific movie showings\n", @@ -212,14 +219,22 @@ " - Viewing customer reservations\n", " - Canceling reservations\n", " \n", + " Movie reviews tools:\n", + " - Getting detailed movie information including plot, cast, ratings\n", + " - Searching for movies across different databases\n", + " - Getting movie reviews and ratings from various sources\n", + " - Getting movie genres and categories\n", + " - Finding random movie recommendations\n", + " \n", " When helping users:\n", - " 1. Use get_current_movies or search_movies to show available options\n", - " 2. Use get_movie_details with exact title, date, time, and room for specific showings\n", + " 1. Use movie reviews tools to get detailed movie information, ratings, and reviews\n", + " 2. Use cinema tools to check showtimes and availability\n", " 3. For reservations, always collect: customer name, email, number of seats wanted\n", " 4. Use make_reservation with the exact movie details from search results\n", - " 5. Be helpful and provide clear information about showtimes, pricing, and availability\n", + " 5. Provide comprehensive movie information including reviews and cinema availability\n", + " 6. Be helpful and guide users through both movie discovery and booking process\n", " \n", - " Always be friendly and guide users through the movie booking process step by step.\"\"\"\n", + " Always be friendly and provide rich information about movies including reviews, ratings, and booking options.\"\"\"\n", " \n", " # Create prompt template\n", " prompt = ChatPromptTemplate.from_messages([\n", @@ -242,12 +257,12 @@ "\n", "# Initialize the agent (now async)\n", "agent_executor = None\n", - "print(\"🤖 Cinema agent setup function ready! Run the next cell to initialize.\")" + "print(\"🤖 Cinema & Movie Reviews agent setup function ready! Run the next cell to initialize.\")" ] }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 7, "id": "688da57b", "metadata": {}, "outputs": [ @@ -255,27 +270,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "Initializing cinema agent with MCP tools...\n", - "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "LangChain cinema agent with MCP tools ready!\n", - "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "LangChain cinema agent with MCP tools ready!\n" + "Initializing cinema and movie reviews agent with MCP tools...\n", + "Error connecting to MCP HTTP servers: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Make sure the MCP servers are running:\n", + "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", + "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n", + "No MCP tools loaded. Make sure the Cinema and Movie Reviews MCP servers are accessible.\n", + "Failed to initialize agent. Check Cinema and Movie Reviews MCP server connections.\n", + "Error connecting to MCP HTTP servers: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Make sure the MCP servers are running:\n", + "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", + "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n", + "No MCP tools loaded. Make sure the Cinema and Movie Reviews MCP servers are accessible.\n", + "Failed to initialize agent. Check Cinema and Movie Reviews MCP server connections.\n" ] } ], "source": [ - "# Initialize the agent with Cinema MCP tools\n", + "# Initialize the agent with Cinema and Movie Reviews MCP tools\n", "async def initialize_agent():\n", - " \"\"\"Initialize the agent with Cinema MCP tools\"\"\"\n", + " \"\"\"Initialize the agent with Cinema and Movie Reviews MCP tools\"\"\"\n", " global agent_executor\n", - " print(\"Initializing cinema agent with MCP tools...\")\n", + " print(\"Initializing cinema and movie reviews agent with MCP tools...\")\n", " agent_executor = await setup_agent()\n", " if agent_executor:\n", - " print(\"LangChain cinema agent with MCP tools ready!\")\n", + " print(\"LangChain cinema and movie reviews agent with MCP tools ready!\")\n", " else:\n", - " print(\"Failed to initialize agent. Check Cinema MCP server connection.\")\n", + " print(\"Failed to initialize agent. Check Cinema and Movie Reviews MCP server connections.\")\n", "\n", "# Run the initialization\n", "await initialize_agent()" @@ -283,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 8, "id": "dc7277da", "metadata": {}, "outputs": [ @@ -291,14 +312,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "💬 Cinema user input handler ready!\n" + "💬 Cinema and Movie Reviews user input handler ready!\n" ] } ], "source": [ "# User Input Handler + logged agent steps\n", "async def process_user_input(user_input: str) -> str:\n", - " \"\"\"Process user input and return LLM response using Cinema MCP tools\"\"\"\n", + " \"\"\"Process user input and return LLM response using Cinema and Movie Reviews MCP tools\"\"\"\n", " if not agent_executor:\n", " return \"Agent not initialized. Please run the initialization cell first.\"\n", " \n", @@ -334,7 +355,7 @@ "\n", "# Interactive function for easy testing\n", "async def ask_assistant(question: str):\n", - " \"\"\"Easy-to-use function for asking the cinema assistant\"\"\"\n", + " \"\"\"Easy-to-use function for asking the cinema and movie reviews assistant\"\"\"\n", " print(f\"🎬 User: {question}\")\n", " print(\"🤖 Assistant:\")\n", " \n", @@ -342,12 +363,12 @@ " print(response)\n", " return response\n", "\n", - "print(\"💬 Cinema user input handler ready!\")" + "print(\"💬 Cinema and Movie Reviews user input handler ready!\")" ] }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 9, "id": "3c70994e", "metadata": {}, "outputs": [ @@ -355,107 +376,40 @@ "name": "stdout", "output_type": "stream", "text": [ - "Loaded 6 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "Cinema MCP HTTP server connected successfully!\n", - "Available tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - " - get_current_movies: Get all currently playing movies with their showtimes and availability\n", - "\n", - "Returns:\n", - " List of all movies currently being shown with details including:\n", - " - Movie title, description, and basic info\n", - " - Showtimes and theater room assignments \n", - " - Seat availability and pricing\n", - " - Genre, rating, and duration\n", - " - Cast and director information\n", - "\n", - " - get_movie_details: Get detailed information about a specific movie showing\n", - "\n", - "Args:\n", - " title: Exact movie title\n", - " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", - " time: Time in HH:MM format (e.g., \"19:30\")\n", - " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", - " \n", - "Returns:\n", - " Detailed movie information including plot, cast, theater details, and availability\n", - " Use search_movies first to find the exact title, date, time, and room values needed\n", - "\n", - " - search_movies: Search for movie presentations with optional filters\n", - "\n", - "Args:\n", - " title: Filter by movie title (partial match, case-insensitive)\n", - " genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family)\n", - " date: Filter by date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", - " room: Filter by cinema room (theater_a, theater_b, theater_c, imax)\n", - " available_seats_min: Minimum number of available seats required\n", - " limit: Maximum number of results to return (default: 20, max: 100)\n", - " \n", - "Returns:\n", - " Filtered list of movie presentations matching the search criteria\n", - "\n", - " - make_reservation: Create a movie reservation for a specific showing\n", - "\n", - "Args:\n", - " title: Exact movie title (use get_movie_details or search_movies to find exact title)\n", - " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", - " time: Time in HH:MM format (e.g., \"19:30\")\n", - " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", - " seats_count: Number of seats to reserve (must be > 0)\n", - " customer_name: Customer's full name\n", - " customer_email: Customer's email address\n", - " customer_phone: Customer's phone number (optional)\n", - " special_requests: Any special requests or accessibility needs (optional)\n", - " \n", - "Returns:\n", - " Reservation confirmation with booking details and pricing information\n", - " Use the exact title, date, time, and room values from search_movies or get_current_movies results\n", - "\n", - " - get_my_reservations: Get all reservations for a customer\n", - "\n", - "Args:\n", - " customer_email: Customer's email address used for reservations\n", - " \n", - "Returns:\n", - " List of all reservations made by the customer\n", - "\n", - " - cancel_reservation: Cancel a movie reservation\n", - "\n", - "Args:\n", - " customer_email: Customer's email address\n", - " title: Exact movie title of the reservation to cancel\n", - " date: Date in YYYY-MM-DD format\n", - " time: Time in HH:MM format\n", - " room: Room identifier\n", - " \n", - "Returns:\n", - " Cancellation confirmation and details about freed seats\n", - " Use get_my_reservations first to find the exact details of reservations to cancel\n", - "\n" + "Error connecting to MCP HTTP servers: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Make sure the MCP servers are running:\n", + "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", + "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n", + "Failed to connect to MCP HTTP servers\n", + "Make sure to start both MCP servers:\n", + "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", + "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n" ] } ], "source": [ - "# Test Cinema MCP server connectivity and tools\n", + "# Test Cinema and Movie Reviews MCP server connectivity and tools\n", "async def test_mcp_connection():\n", - " \"\"\"Test Cinema MCP server connection and list available tools\"\"\"\n", + " \"\"\"Test Cinema and Movie Reviews MCP server connections and list available tools\"\"\"\n", " tools = await create_mcp_tools()\n", " if tools:\n", - " print(f\"Cinema MCP HTTP server connected successfully!\")\n", + " print(f\"Cinema and Movie Reviews MCP HTTP servers connected successfully!\")\n", " print(f\"Available tools: {[tool.name for tool in tools]}\")\n", " for tool in tools:\n", " print(f\" - {tool.name}: {tool.description}\")\n", " else:\n", - " print(\"Failed to connect to Cinema MCP HTTP server\")\n", - " print(\"Make sure to start the cinema MCP server first:\")\n", - " print(\"cd src/mcp/cinema-mcp && uv run mcp dev main.py\")\n", + " print(\"Failed to connect to MCP HTTP servers\")\n", + " print(\"Make sure to start both MCP servers:\")\n", + " print(\"Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\")\n", + " print(\"Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\")\n", "\n", - "# Test Cinema MCP HTTP connection\n", + "# Test MCP HTTP connections\n", "await test_mcp_connection()" ] }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 10, "id": "de62dd60", "metadata": {}, "outputs": [ @@ -463,16 +417,16 @@ "name": "stdout", "output_type": "stream", "text": [ - "🔍 Debugging Cinema MCP connection...\n", - "✅ Basic HTTP connection works: 404\n", + "🔍 Debugging Cinema and Movie Reviews MCP connections...\n", + "❌ Cinema HTTP connection failed: All connection attempts failed\n", + "❌ Cinema HTTP connection failed: All connection attempts failed\n", + "❌ Movie Reviews HTTP connection failed: All connection attempts failed\n", "✅ MCP client created successfully\n", - "✅ Got 6 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", - "Full traceback:\n", - "✅ Basic HTTP connection works: 404\n", + "❌ Movie Reviews HTTP connection failed: All connection attempts failed\n", "✅ MCP client created successfully\n", - "✅ Got 6 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation']\n", - "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", + "❌ Error getting tools: unhandled errors in a TaskGroup (1 sub-exception)\n", + "Full traceback:\n", + "❌ Error getting tools: unhandled errors in a TaskGroup (1 sub-exception)\n", "Full traceback:\n" ] }, @@ -480,31 +434,160 @@ "name": "stderr", "output_type": "stream", "text": [ - "Traceback (most recent call last):\n", - " File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_27352\\2795222468.py\", line 37, in debug_mcp_connection\n", - " await client.close()\n", - " ^^^^^^^^^^^^\n", - "AttributeError: 'MultiServerMCPClient' object has no attribute 'close'\n" + " + Exception Group Traceback (most recent call last):\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_38108\\2853036564.py\", line 42, in debug_mcp_connection\n", + " | tools = await client.get_tools()\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\client.py\", line 157, in get_tools\n", + " | tools_list = await asyncio.gather(*load_mcp_tool_tasks)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\tools.py\", line 188, in load_mcp_tools\n", + " | async with create_session(connection) as tool_session:\n", + " | ~~~~~~~~~~~~~~^^^^^^^^^^^^\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 235, in __aexit__\n", + " | await self.gen.athrow(value)\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\sessions.py\", line 394, in create_session\n", + " | async with _create_streamable_http_session(**params) as session:\n", + " | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 235, in __aexit__\n", + " | await self.gen.athrow(value)\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\sessions.py\", line 309, in _create_streamable_http_session\n", + " | streamablehttp_client(\n", + " | ~~~~~~~~~~~~~~~~~~~~~^\n", + " | url,\n", + " | ^^^^\n", + " | ...<5 lines>...\n", + " | **kwargs,\n", + " | ^^^^^^^^^\n", + " | ) as (read, write, _),\n", + " | ^\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 235, in __aexit__\n", + " | await self.gen.athrow(value)\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\mcp\\client\\streamable_http.py\", line 478, in streamablehttp_client\n", + " | async with anyio.create_task_group() as tg:\n", + " | ~~~~~~~~~~~~~~~~~~~~~~~^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\anyio\\_backends\\_asyncio.py\", line 781, in __aexit__\n", + " | raise BaseExceptionGroup(\n", + " | \"unhandled errors in a TaskGroup\", self._exceptions\n", + " | ) from None\n", + " | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)\n", + " +-+---------------- 1 ----------------\n", + " | Traceback (most recent call last):\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 101, in map_httpcore_exceptions\n", + " | yield\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 394, in handle_async_request\n", + " | resp = await self._pool.handle_async_request(req)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection_pool.py\", line 256, in handle_async_request\n", + " | raise exc from None\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection_pool.py\", line 236, in handle_async_request\n", + " | response = await connection.handle_async_request(\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | pool_request.request\n", + " | ^^^^^^^^^^^^^^^^^^^^\n", + " | )\n", + " | ^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection.py\", line 101, in handle_async_request\n", + " | raise exc\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection.py\", line 78, in handle_async_request\n", + " | stream = await self._connect(request)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection.py\", line 124, in _connect\n", + " | stream = await self._network_backend.connect_tcp(**kwargs)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_backends\\auto.py\", line 31, in connect_tcp\n", + " | return await self._backend.connect_tcp(\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | ...<5 lines>...\n", + " | )\n", + " | ^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_backends\\anyio.py\", line 113, in connect_tcp\n", + " | with map_exceptions(exc_map):\n", + " | ~~~~~~~~~~~~~~^^^^^^^^^\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 162, in __exit__\n", + " | self.gen.throw(value)\n", + " | ~~~~~~~~~~~~~~^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_exceptions.py\", line 14, in map_exceptions\n", + " | raise to_exc(exc) from exc\n", + " | httpcore.ConnectError: All connection attempts failed\n", + " | \n", + " | The above exception was the direct cause of the following exception:\n", + " | \n", + " | Traceback (most recent call last):\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\mcp\\client\\streamable_http.py\", line 409, in handle_request_async\n", + " | await self._handle_post_request(ctx)\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\mcp\\client\\streamable_http.py\", line 260, in _handle_post_request\n", + " | async with ctx.client.stream(\n", + " | ~~~~~~~~~~~~~~~~~^\n", + " | \"POST\",\n", + " | ^^^^^^^\n", + " | ...<2 lines>...\n", + " | headers=headers,\n", + " | ^^^^^^^^^^^^^^^^\n", + " | ) as response:\n", + " | ^\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 214, in __aenter__\n", + " | return await anext(self.gen)\n", + " | ^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1583, in stream\n", + " | response = await self.send(\n", + " | ^^^^^^^^^^^^^^^^\n", + " | ...<4 lines>...\n", + " | )\n", + " | ^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1629, in send\n", + " | response = await self._send_handling_auth(\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | ...<4 lines>...\n", + " | )\n", + " | ^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1657, in _send_handling_auth\n", + " | response = await self._send_handling_redirects(\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | ...<3 lines>...\n", + " | )\n", + " | ^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1694, in _send_handling_redirects\n", + " | response = await self._send_single_request(request)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1730, in _send_single_request\n", + " | response = await transport.handle_async_request(request)\n", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 393, in handle_async_request\n", + " | with map_httpcore_exceptions():\n", + " | ~~~~~~~~~~~~~~~~~~~~~~~^^\n", + " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 162, in __exit__\n", + " | self.gen.throw(value)\n", + " | ~~~~~~~~~~~~~~^^^^^^^\n", + " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 118, in map_httpcore_exceptions\n", + " | raise mapped_exc(message) from exc\n", + " | httpx.ConnectError: All connection attempts failed\n", + " +------------------------------------\n" ] } ], "source": [ - "# Detailed debug of MCP connection\n", + "# Detailed debug of MCP connections\n", "import traceback\n", "import httpx\n", "\n", "async def debug_mcp_connection():\n", - " \"\"\"Debug the Cinema MCP connection with detailed error info\"\"\"\n", - " print(\"🔍 Debugging Cinema MCP connection...\")\n", + " \"\"\"Debug the Cinema and Movie Reviews MCP connections with detailed error info\"\"\"\n", + " print(\"🔍 Debugging Cinema and Movie Reviews MCP connections...\")\n", " \n", - " # Test basic HTTP connectivity first\n", - " try:\n", - " async with httpx.AsyncClient() as client:\n", - " response = await client.get(\"http://localhost:8010\", timeout=5.0)\n", - " print(f\"✅ Basic HTTP connection works: {response.status_code}\")\n", - " except Exception as e:\n", - " print(f\"❌ Basic HTTP connection failed: {e}\")\n", - " return\n", + " # Test basic HTTP connectivity for both servers\n", + " servers = [\n", + " (\"Cinema\", \"http://localhost:8010\"),\n", + " (\"Movie Reviews\", \"http://localhost:8011\")\n", + " ]\n", + " \n", + " for server_name, url in servers:\n", + " try:\n", + " async with httpx.AsyncClient() as client:\n", + " response = await client.get(url, timeout=5.0)\n", + " print(f\"✅ {server_name} HTTP connection works: {response.status_code}\")\n", + " except Exception as e:\n", + " print(f\"❌ {server_name} HTTP connection failed: {e}\")\n", " \n", " # Test MCP client creation with detailed error tracking\n", " try:\n", @@ -514,6 +597,10 @@ " \"cinema\": {\n", " \"transport\": \"streamable_http\",\n", " \"url\": os.getenv(\"CINEMA_MCP_URL\")\n", + " },\n", + " \"movie_reviews\": {\n", + " \"transport\": \"streamable_http\",\n", + " \"url\": os.getenv(\"MOVIES_MCP_URL\")\n", " }\n", " })\n", " \n", @@ -544,7 +631,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 11, "id": "1551f227", "metadata": {}, "outputs": [ @@ -554,638 +641,7 @@ "text": [ "🎬 User: What movies are currently playing?\n", "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_current_movies` with `{}`\n", - "responded: Sure! Let me check the list of movies currently playing along with their showtimes and availability for you.\n", - "\n", - "\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_current_movies` with `{}`\n", - "responded: Sure! Let me check the list of movies currently playing along with their showtimes and availability for you.\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"cinema_name\": \"MovieMagic Cinema\",\n", - " \"total_movies\": 8,\n", - " \"movies\": [\n", - " {\n", - " \"title\": \"Galactic Adventures\",\n", - " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"14:30\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 105,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 142,\n", - " \"genre\": \"Science Fiction\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Sarah Johnson\",\n", - " \"cast\": [\n", - " \"Alex Thompson\",\n", - " \"Maria Rodriguez\",\n", - " \"James Chen\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 30.0\n", - " },\n", - " {\n", - " \"title\": \"The Midnight Mystery\",\n", - " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"19:15\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 75,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 118,\n", - " \"genre\": \"Thriller\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Michael Davis\",\n", - " \"cast\": [\n", - " \"Emma Wilson\",\n", - " \"Robert Garcia\",\n", - " \"Lisa Park\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 62.5\n", - " },\n", - " {\n", - " \"title\": \"Laugh Out Loud\",\n", - " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 60,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 95,\n", - " \"genre\": \"Comedy\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Jennifer Lee\",\n", - " \"cast\": [\n", - " \"Tom Martinez\",\n", - " \"Sarah Kim\",\n", - " \"David Brown\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 60.0\n", - " },\n", - " {\n", - " \"title\": \"Dragon's Heart\",\n", - " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"10:00\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 77,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 103,\n", - " \"genre\": \"Animation\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 10.0,\n", - " \"director\": \"Animation Studios Inc.\",\n", - " \"cast\": [\n", - " \"Voice Cast: Amy Johnson\",\n", - " \"Mark Stevens\",\n", - " \"Luna Rodriguez\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 23.0\n", - " },\n", - " {\n", - " \"title\": \"City of Shadows\",\n", - " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"16:20\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 44,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 134,\n", - " \"genre\": \"Drama\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Vincent Romano\",\n", - " \"cast\": [\n", - " \"Antonio Silva\",\n", - " \"Catherine Moore\",\n", - " \"Frank Williams\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 78.0\n", - " },\n", - " {\n", - " \"title\": \"Ocean's Edge\",\n", - " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"20:00\",\n", - " \"room\": \"IMAX Theater\",\n", - " \"seats_remaining\": 222,\n", - " \"seats_total\": 300,\n", - " \"duration_minutes\": 87,\n", - " \"genre\": \"Documentary\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 18.0,\n", - " \"director\": \"Ocean Explorer Films\",\n", - " \"cast\": [\n", - " \"Narrator: David Attenborough\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 26.0\n", - " },\n", - " {\n", - " \"title\": \"Love in Paris\",\n", - " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 31,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 108,\n", - " \"genre\": \"Romance\",\n", - " \"rating\": \"PG\",\n", - " \"price_per_seat\": 11.5,\n", - " \"director\": \"Claire Dubois\",\n", - " \"cast\": [\n", - " \"Sophie Martin\",\n", - " \"Jean-Luc Moreau\",\n", - " \"Isabella Jones\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 69.0\n", - " },\n", - " {\n", - " \"title\": \"Nightmare Manor\",\n", - " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"22:30\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 58,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 106,\n", - " \"genre\": \"Horror\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 13.0,\n", - " \"director\": \"Horror Productions\",\n", - " \"cast\": [\n", - " \"Scary Actor 1\",\n", - " \"Scary Actor 2\",\n", - " \"Scary Actor 3\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 61.3\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"cinema_name\": \"MovieMagic Cinema\",\n", - " \"total_movies\": 8,\n", - " \"movies\": [\n", - " {\n", - " \"title\": \"Galactic Adventures\",\n", - " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"14:30\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 105,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 142,\n", - " \"genre\": \"Science Fiction\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Sarah Johnson\",\n", - " \"cast\": [\n", - " \"Alex Thompson\",\n", - " \"Maria Rodriguez\",\n", - " \"James Chen\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 30.0\n", - " },\n", - " {\n", - " \"title\": \"The Midnight Mystery\",\n", - " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"19:15\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 75,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 118,\n", - " \"genre\": \"Thriller\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Michael Davis\",\n", - " \"cast\": [\n", - " \"Emma Wilson\",\n", - " \"Robert Garcia\",\n", - " \"Lisa Park\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 62.5\n", - " },\n", - " {\n", - " \"title\": \"Laugh Out Loud\",\n", - " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 60,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 95,\n", - " \"genre\": \"Comedy\",\n", - " \"rating\": \"PG-13\",\n", - " \"price_per_seat\": 12.5,\n", - " \"director\": \"Jennifer Lee\",\n", - " \"cast\": [\n", - " \"Tom Martinez\",\n", - " \"Sarah Kim\",\n", - " \"David Brown\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 60.0\n", - " },\n", - " {\n", - " \"title\": \"Dragon's Heart\",\n", - " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"10:00\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 77,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 103,\n", - " \"genre\": \"Animation\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 10.0,\n", - " \"director\": \"Animation Studios Inc.\",\n", - " \"cast\": [\n", - " \"Voice Cast: Amy Johnson\",\n", - " \"Mark Stevens\",\n", - " \"Luna Rodriguez\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 23.0\n", - " },\n", - " {\n", - " \"title\": \"City of Shadows\",\n", - " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"16:20\",\n", - " \"room\": \"Theater B\",\n", - " \"seats_remaining\": 44,\n", - " \"seats_total\": 200,\n", - " \"duration_minutes\": 134,\n", - " \"genre\": \"Drama\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 14.0,\n", - " \"director\": \"Vincent Romano\",\n", - " \"cast\": [\n", - " \"Antonio Silva\",\n", - " \"Catherine Moore\",\n", - " \"Frank Williams\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 78.0\n", - " },\n", - " {\n", - " \"title\": \"Ocean's Edge\",\n", - " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", - " \"date\": \"2025-09-26\",\n", - " \"time\": \"20:00\",\n", - " \"room\": \"IMAX Theater\",\n", - " \"seats_remaining\": 222,\n", - " \"seats_total\": 300,\n", - " \"duration_minutes\": 87,\n", - " \"genre\": \"Documentary\",\n", - " \"rating\": \"G\",\n", - " \"price_per_seat\": 18.0,\n", - " \"director\": \"Ocean Explorer Films\",\n", - " \"cast\": [\n", - " \"Narrator: David Attenborough\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 26.0\n", - " },\n", - " {\n", - " \"title\": \"Love in Paris\",\n", - " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_remaining\": 31,\n", - " \"seats_total\": 100,\n", - " \"duration_minutes\": 108,\n", - " \"genre\": \"Romance\",\n", - " \"rating\": \"PG\",\n", - " \"price_per_seat\": 11.5,\n", - " \"director\": \"Claire Dubois\",\n", - " \"cast\": [\n", - " \"Sophie Martin\",\n", - " \"Jean-Luc Moreau\",\n", - " \"Isabella Jones\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 69.0\n", - " },\n", - " {\n", - " \"title\": \"Nightmare Manor\",\n", - " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"22:30\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_remaining\": 58,\n", - " \"seats_total\": 150,\n", - " \"duration_minutes\": 106,\n", - " \"genre\": \"Horror\",\n", - " \"rating\": \"R\",\n", - " \"price_per_seat\": 13.0,\n", - " \"director\": \"Horror Productions\",\n", - " \"cast\": [\n", - " \"Scary Actor 1\",\n", - " \"Scary Actor 2\",\n", - " \"Scary Actor 3\"\n", - " ],\n", - " \"is_sold_out\": false,\n", - " \"occupancy_percentage\": 61.3\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[32;1m\u001b[1;3mHere’s what’s currently playing at **MovieMagic Cinema**:\n", - "\n", - "### 🎬 **Today — September 25, 2025**\n", - "1. **Galactic Adventures** \n", - " *Science Fiction | PG-13 | 142 min* \n", - " ⏰ 14:30 — Theater A \n", - " 💺 105 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Epic space journey to save humanity from an alien threat.*\n", - "\n", - "2. **The Midnight Mystery** \n", - " *Thriller | R | 118 min* \n", - " ⏰ 19:15 — Theater B \n", - " 💺 75 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Small town sheriff investigates midnight disappearances.*\n", - "\n", - "3. **Laugh Out Loud** \n", - " *Comedy | PG-13 | 95 min* \n", - " ⏰ 21:45 — Theater A \n", - " 💺 60 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Three friends accidentally become viral internet stars.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 26, 2025**\n", - "4. **Dragon's Heart** \n", - " *Animation | G | 103 min* \n", - " ⏰ 10:00 — Theater C \n", - " 💺 77 / 100 seats available \n", - " 🎟 $10.00 per seat \n", - " *Young girl must save her village with help from dragons.*\n", - "\n", - "5. **City of Shadows** \n", - " *Drama | R | 134 min* \n", - " ⏰ 16:20 — Theater B \n", - " 💺 44 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Noir detective uncovers police corruption in 1940s NYC.*\n", - "\n", - "6. **Ocean's Edge** \n", - " *Documentary | G | 87 min* \n", - " ⏰ 20:00 — IMAX Theater \n", - " 💺 222 / 300 seats available \n", - " 🎟 $18.00 per seat \n", - " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** \n", - " *Romance | PG | 108 min* \n", - " ⏰ 15:45 — Theater C \n", - " 💺 31 / 100 seats available \n", - " 🎟 $11.50 per seat \n", - " *American tourist finds love with a Paris café owner.*\n", - "\n", - "8. **Nightmare Manor** \n", - " *Horror | R | 106 min* \n", - " ⏰ 22:30 — Theater A \n", - " 💺 58 / 150 seats available \n", - " 🎟 $13.00 per seat \n", - " *Family inherits a haunted mansion with dark secrets.*\n", - "\n", - "---\n", - "\n", - "Would you like me to show more details for any of these movies or help you make a reservation?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here’s what’s currently playing at **MovieMagic Cinema**:\n", - "\n", - "### 🎬 **Today — September 25, 2025**\n", - "1. **Galactic Adventures** \n", - " *Science Fiction | PG-13 | 142 min* \n", - " ⏰ 14:30 — Theater A \n", - " 💺 105 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Epic space journey to save humanity from an alien threat.*\n", - "\n", - "2. **The Midnight Mystery** \n", - " *Thriller | R | 118 min* \n", - " ⏰ 19:15 — Theater B \n", - " 💺 75 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Small town sheriff investigates midnight disappearances.*\n", - "\n", - "3. **Laugh Out Loud** \n", - " *Comedy | PG-13 | 95 min* \n", - " ⏰ 21:45 — Theater A \n", - " 💺 60 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Three friends accidentally become viral internet stars.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 26, 2025**\n", - "4. **Dragon's Heart** \n", - " *Animation | G | 103 min* \n", - " ⏰ 10:00 — Theater C \n", - " 💺 77 / 100 seats available \n", - " 🎟 $10.00 per seat \n", - " *Young girl must save her village with help from dragons.*\n", - "\n", - "5. **City of Shadows** \n", - " *Drama | R | 134 min* \n", - " ⏰ 16:20 — Theater B \n", - " 💺 44 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Noir detective uncovers police corruption in 1940s NYC.*\n", - "\n", - "6. **Ocean's Edge** \n", - " *Documentary | G | 87 min* \n", - " ⏰ 20:00 — IMAX Theater \n", - " 💺 222 / 300 seats available \n", - " 🎟 $18.00 per seat \n", - " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** \n", - " *Romance | PG | 108 min* \n", - " ⏰ 15:45 — Theater C \n", - " 💺 31 / 100 seats available \n", - " 🎟 $11.50 per seat \n", - " *American tourist finds love with a Paris café owner.*\n", - "\n", - "8. **Nightmare Manor** \n", - " *Horror | R | 106 min* \n", - " ⏰ 22:30 — Theater A \n", - " 💺 58 / 150 seats available \n", - " 🎟 $13.00 per seat \n", - " *Family inherits a haunted mansion with dark secrets.*\n", - "\n", - "---\n", - "\n", - "Would you like me to show more details for any of these movies or help you make a reservation?\n", - "\n", - "==================================================\n", - "\n", - "\u001b[32;1m\u001b[1;3mHere’s what’s currently playing at **MovieMagic Cinema**:\n", - "\n", - "### 🎬 **Today — September 25, 2025**\n", - "1. **Galactic Adventures** \n", - " *Science Fiction | PG-13 | 142 min* \n", - " ⏰ 14:30 — Theater A \n", - " 💺 105 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Epic space journey to save humanity from an alien threat.*\n", - "\n", - "2. **The Midnight Mystery** \n", - " *Thriller | R | 118 min* \n", - " ⏰ 19:15 — Theater B \n", - " 💺 75 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Small town sheriff investigates midnight disappearances.*\n", - "\n", - "3. **Laugh Out Loud** \n", - " *Comedy | PG-13 | 95 min* \n", - " ⏰ 21:45 — Theater A \n", - " 💺 60 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Three friends accidentally become viral internet stars.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 26, 2025**\n", - "4. **Dragon's Heart** \n", - " *Animation | G | 103 min* \n", - " ⏰ 10:00 — Theater C \n", - " 💺 77 / 100 seats available \n", - " 🎟 $10.00 per seat \n", - " *Young girl must save her village with help from dragons.*\n", - "\n", - "5. **City of Shadows** \n", - " *Drama | R | 134 min* \n", - " ⏰ 16:20 — Theater B \n", - " 💺 44 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Noir detective uncovers police corruption in 1940s NYC.*\n", - "\n", - "6. **Ocean's Edge** \n", - " *Documentary | G | 87 min* \n", - " ⏰ 20:00 — IMAX Theater \n", - " 💺 222 / 300 seats available \n", - " 🎟 $18.00 per seat \n", - " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** \n", - " *Romance | PG | 108 min* \n", - " ⏰ 15:45 — Theater C \n", - " 💺 31 / 100 seats available \n", - " 🎟 $11.50 per seat \n", - " *American tourist finds love with a Paris café owner.*\n", - "\n", - "8. **Nightmare Manor** \n", - " *Horror | R | 106 min* \n", - " ⏰ 22:30 — Theater A \n", - " 💺 58 / 150 seats available \n", - " 🎟 $13.00 per seat \n", - " *Family inherits a haunted mansion with dark secrets.*\n", - "\n", - "---\n", - "\n", - "Would you like me to show more details for any of these movies or help you make a reservation?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here’s what’s currently playing at **MovieMagic Cinema**:\n", - "\n", - "### 🎬 **Today — September 25, 2025**\n", - "1. **Galactic Adventures** \n", - " *Science Fiction | PG-13 | 142 min* \n", - " ⏰ 14:30 — Theater A \n", - " 💺 105 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Epic space journey to save humanity from an alien threat.*\n", - "\n", - "2. **The Midnight Mystery** \n", - " *Thriller | R | 118 min* \n", - " ⏰ 19:15 — Theater B \n", - " 💺 75 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Small town sheriff investigates midnight disappearances.*\n", - "\n", - "3. **Laugh Out Loud** \n", - " *Comedy | PG-13 | 95 min* \n", - " ⏰ 21:45 — Theater A \n", - " 💺 60 / 150 seats available \n", - " 🎟 $12.50 per seat \n", - " *Three friends accidentally become viral internet stars.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 26, 2025**\n", - "4. **Dragon's Heart** \n", - " *Animation | G | 103 min* \n", - " ⏰ 10:00 — Theater C \n", - " 💺 77 / 100 seats available \n", - " 🎟 $10.00 per seat \n", - " *Young girl must save her village with help from dragons.*\n", - "\n", - "5. **City of Shadows** \n", - " *Drama | R | 134 min* \n", - " ⏰ 16:20 — Theater B \n", - " 💺 44 / 200 seats available \n", - " 🎟 $14.00 per seat \n", - " *Noir detective uncovers police corruption in 1940s NYC.*\n", - "\n", - "6. **Ocean's Edge** \n", - " *Documentary | G | 87 min* \n", - " ⏰ 20:00 — IMAX Theater \n", - " 💺 222 / 300 seats available \n", - " 🎟 $18.00 per seat \n", - " *Breathtaking IMAX dive into the deepest parts of our oceans.*\n", - "\n", - "---\n", - "\n", - "### 📅 **September 27, 2025**\n", - "7. **Love in Paris** \n", - " *Romance | PG | 108 min* \n", - " ⏰ 15:45 — Theater C \n", - " 💺 31 / 100 seats available \n", - " 🎟 $11.50 per seat \n", - " *American tourist finds love with a Paris café owner.*\n", - "\n", - "8. **Nightmare Manor** \n", - " *Horror | R | 106 min* \n", - " ⏰ 22:30 — Theater A \n", - " 💺 58 / 150 seats available \n", - " 🎟 $13.00 per seat \n", - " *Family inherits a haunted mansion with dark secrets.*\n", - "\n", - "---\n", - "\n", - "Would you like me to show more details for any of these movies or help you make a reservation?\n", + "Agent not initialized. Please run the initialization cell first.\n", "\n", "==================================================\n", "\n" @@ -1206,7 +662,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 12, "id": "9295d748", "metadata": {}, "outputs": [ @@ -1214,660 +670,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "🎬 Welcome to MovieMagic Cinema Assistant!\n", - "I can help you find movies, check showtimes, and make reservations.\n", + "🎬 Welcome to MovieMagic Cinema & Reviews Assistant!\n", + "I can help you find movies, check reviews, get ratings, check showtimes, and make reservations.\n", "Type 'exit' to quit. Press Enter on an empty line to skip.\n", "\n", - "🎬 User: Do I have any reservations?\n", + "🎬 User: Hello\n", "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "🎬 User: Do I have any reservations?\n", + "Agent not initialized. Please run the initialization cell first.\n", + "🎬 User: Hello\n", "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3mSure! Could you please provide the **email address** you used for your reservations so I can look them up?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Sure! Could you please provide the **email address** you used for your reservations so I can look them up?\n", - "\u001b[32;1m\u001b[1;3mSure! Could you please provide the **email address** you used for your reservations so I can look them up?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Sure! Could you please provide the **email address** you used for your reservations so I can look them up?\n", - "🎬 User: kontakt@andrzejpytel.pl\n", - "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "🎬 User: kontakt@andrzejpytel.pl\n", - "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"total_reservations\": 3,\n", - " \"reservations\": [\n", - " {\n", - " \"reservation_number\": 1,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 2,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 3,\n", - " \"movie_title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", - " \"special_requests\": null\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"total_reservations\": 3,\n", - " \"reservations\": [\n", - " {\n", - " \"reservation_number\": 1,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 2,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 3,\n", - " \"movie_title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", - " \"special_requests\": null\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are your current reservations:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", - "\n", - "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are your current reservations:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", - "\n", - "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\n", - "\u001b[32;1m\u001b[1;3mHere are your current reservations:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", - "\n", - "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are your current reservations:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - "\n", - "It looks like you have two separate single-seat bookings for *Love in Paris*. \n", - "\n", - "Would you like me to **combine seats**, **cancel any**, or keep them as they are?\n", - "🎬 User: get my reservations please\n", - "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "🎬 User: get my reservations please\n", - "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"total_reservations\": 3,\n", - " \"reservations\": [\n", - " {\n", - " \"reservation_number\": 1,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 2,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 3,\n", - " \"movie_title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", - " \"special_requests\": null\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"total_reservations\": 3,\n", - " \"reservations\": [\n", - " {\n", - " \"reservation_number\": 1,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 2,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 3,\n", - " \"movie_title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", - " \"special_requests\": null\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are your current reservations: \n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "---\n", - "\n", - "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", - "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are your current reservations: \n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "---\n", - "\n", - "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", - "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\n", - "\u001b[32;1m\u001b[1;3mHere are your current reservations: \n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "---\n", - "\n", - "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", - "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are your current reservations: \n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "---\n", - "\n", - "It looks like you have **two separate single-seat bookings** for *Love in Paris*. \n", - "Would you like me to **merge them into one reservation for 2 seats**, or cancel any?\n", - "🎬 User: Check reservations for kontakt@andrzejpytel.pl\n", - "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "🎬 User: Check reservations for kontakt@andrzejpytel.pl\n", - "🤖 Assistant:\n", - "\n", - "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[32;1m\u001b[1;3m\n", - "Invoking: `get_my_reservations` with `{'customer_email': 'kontakt@andrzejpytel.pl'}`\n", - "\n", - "\n", - "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"total_reservations\": 3,\n", - " \"reservations\": [\n", - " {\n", - " \"reservation_number\": 1,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 2,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 3,\n", - " \"movie_title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", - " \"special_requests\": null\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", - " \"customer_email\": \"kontakt@andrzejpytel.pl\",\n", - " \"total_reservations\": 3,\n", - " \"reservations\": [\n", - " {\n", - " \"reservation_number\": 1,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:02.316609\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 2,\n", - " \"movie_title\": \"Love in Paris\",\n", - " \"date\": \"2025-09-27\",\n", - " \"time\": \"15:45\",\n", - " \"room\": \"Theater C\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T13:55:52.947823\",\n", - " \"special_requests\": null\n", - " },\n", - " {\n", - " \"reservation_number\": 3,\n", - " \"movie_title\": \"Laugh Out Loud\",\n", - " \"date\": \"2025-09-25\",\n", - " \"time\": \"21:45\",\n", - " \"room\": \"Theater A\",\n", - " \"seats_reserved\": 1,\n", - " \"status\": \"confirmed\",\n", - " \"reservation_made\": \"2025-09-24T14:02:09.153700\",\n", - " \"special_requests\": null\n", - " }\n", - " ]\n", - "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the reservations for **kontakt@andrzejpytel.pl**:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", - "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are the reservations for **kontakt@andrzejpytel.pl**:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", - "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\n", - "\u001b[32;1m\u001b[1;3mHere are the reservations for **kontakt@andrzejpytel.pl**:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", - "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n", - "Here are the reservations for **kontakt@andrzejpytel.pl**:\n", - "\n", - "1. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "2. **Love in Paris** \n", - " 📅 Date: September 27, 2025 \n", - " ⏰ Time: 15:45 \n", - " 🎭 Room: Theater C \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 13:55 \n", - "\n", - "3. **Laugh Out Loud** \n", - " 📅 Date: September 25, 2025 \n", - " ⏰ Time: 21:45 \n", - " 🎭 Room: Theater A \n", - " 🎟 Seats reserved: 1 \n", - " ✅ Status: Confirmed \n", - " 📌 Reserved on: September 24, 2025, 14:02 \n", - "\n", - "It seems you have **two separate single-seat reservations** for *Love in Paris*. \n", - "Would you like me to combine them into a single reservation for 2 seats, or leave them as they are?\n", - "🎬 Thanks for using MovieMagic Cinema! Goodbye!\n", - "🎬 Thanks for using MovieMagic Cinema! Goodbye!\n" + "Agent not initialized. Please run the initialization cell first.\n", + "🎬 Thanks for using MovieMagic Cinema & Reviews! Goodbye!\n", + "🎬 Thanks for using MovieMagic Cinema & Reviews! Goodbye!\n" ] } ], @@ -1876,15 +690,17 @@ "\n", "# Try these example questions:\n", "# - \"What movies are playing today?\"\n", - "# - \"Show me action movies\"\n", - "# - \"I want to see Avatar tomorrow evening\"\n", + "# - \"Show me action movies and their reviews\"\n", + "# - \"Tell me about Avatar - plot, cast, and reviews\"\n", + "# - \"I want to see Avatar tomorrow evening - show me reviews and showtimes\"\n", "# - \"Get me details for Avatar on 2025-09-25 at 19:30 in theater_a\"\n", "# - \"Book 2 seats for Avatar on 2025-09-25 at 19:30 in theater_a for John Doe, john@email.com\"\n", "# - \"Show my reservations for john@email.com\"\n", + "# - \"Find me a random movie recommendation with good reviews\"\n", "\n", "async def chat_loop():\n", - " print(\"🎬 Welcome to MovieMagic Cinema Assistant!\")\n", - " print(\"I can help you find movies, check showtimes, and make reservations.\")\n", + " print(\"🎬 Welcome to MovieMagic Cinema & Reviews Assistant!\")\n", + " print(\"I can help you find movies, check reviews, get ratings, check showtimes, and make reservations.\")\n", " print(\"Type 'exit' to quit. Press Enter on an empty line to skip.\\n\")\n", " \n", " while True:\n", @@ -1896,17 +712,17 @@ " if not question:\n", " continue\n", " if question.lower() in (\"exit\", \"quit\", \"q\"):\n", - " print(\"🎬 Thanks for using MovieMagic Cinema! Goodbye!\")\n", + " print(\"🎬 Thanks for using MovieMagic Cinema & Reviews! Goodbye!\")\n", " break\n", " await ask_assistant(question)\n", "\n", - "# Start the cinema chat loop\n", + "# Start the cinema and movie reviews chat loop\n", "await chat_loop()" ] }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 13, "id": "e70dad2d", "metadata": {}, "outputs": [ @@ -1919,16 +735,16 @@ } ], "source": [ - "# Cleanup function for HTTP MCP client\n", + "# Cleanup function for HTTP MCP clients\n", "async def cleanup_mcp():\n", " \"\"\"Cleanup MCP client and server resources\"\"\"\n", " global mcp_client\n", " if mcp_client:\n", " try:\n", " await mcp_client.close()\n", - " print(\"Cinema MCP client closed\")\n", + " print(\"Cinema and Movie Reviews MCP clients closed\")\n", " except Exception as e:\n", - " print(f\"Warning: Error closing Cinema MCP client: {e}\")\n", + " print(f\"Warning: Error closing MCP clients: {e}\")\n", " mcp_client = None\n", "\n", "print(\"🧹 Cleanup function ready!\")" diff --git a/src/mcp/movie-reviews-mcp/uv.lock b/src/mcp/movie-reviews-mcp/uv.lock index 0ada2fd..df0f880 100644 --- a/src/mcp/movie-reviews-mcp/uv.lock +++ b/src/mcp/movie-reviews-mcp/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -24,21 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] -[[package]] -name = "attractions-mcp" -version = "1.0.0" -source = { virtual = "." } -dependencies = [ - { name = "mcp", extra = ["cli"] }, - { name = "requests" }, -] - -[package.metadata] -requires-dist = [ - { name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }, - { name = "requests" }, -] - [[package]] name = "attrs" version = "25.3.0" @@ -240,6 +225,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "movie-reviews-mcp" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }, + { name = "requests" }, +] + [[package]] name = "pydantic" version = "2.11.7" From 7f560edfe643f02e7f807613047e0b4156528275 Mon Sep 17 00:00:00 2001 From: Justin Chew Date: Wed, 24 Sep 2025 14:57:21 +0100 Subject: [PATCH 19/25] fix movie reviews model --- src/mcp/movie-reviews-mcp/models.py | 2 +- src/mcp/movie-reviews-mcp/movie_service.py | 2 +- src/mcp/movie-reviews-mcp/utils.py | 14 ++++------ src/mcp/movie-reviews-mcp/uv.lock | 32 +++++++++++----------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/models.py b/src/mcp/movie-reviews-mcp/models.py index 4ce56c7..adce83b 100644 --- a/src/mcp/movie-reviews-mcp/models.py +++ b/src/mcp/movie-reviews-mcp/models.py @@ -24,7 +24,7 @@ class Movie: rating: float durationMins: int year: int - genres: List[str] + genre: str @dataclass class MoviesList: diff --git a/src/mcp/movie-reviews-mcp/movie_service.py b/src/mcp/movie-reviews-mcp/movie_service.py index 42c633f..bfc88e8 100644 --- a/src/mcp/movie-reviews-mcp/movie_service.py +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -40,7 +40,7 @@ def search_movies_data( elif limit < 1: limit = 1 - data = search_movies( genre, limit) + data = search_movies(genre, limit) if not data: return {"error": "No movies found matching the criteria"} diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py index ef6f77f..8584518 100644 --- a/src/mcp/movie-reviews-mcp/utils.py +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -31,7 +31,6 @@ def get_movie_by_id(movie_id: int) -> Optional[Dict[str, Any]]: def search_movies( - title: Optional[str] = None, genre: Optional[str] = None, limit: int = 20 ) -> Optional[Dict[str, Any]]: @@ -43,14 +42,9 @@ def search_movies( # Filter by genre genre_match = True if genre: - genre_match = movie["genre"] == genre.lower() + genre_match = movie["genres"] == genre.lower() - # Filter by title - title_match = True - if title: - title_match = movie["title"] == title.lower() - - if genre_match and title_match: + if genre_match: filtered_movies.append(movie) # Apply limit @@ -74,9 +68,11 @@ def parse_movie_data(data: Dict[str, Any]) -> Movie: return Movie( id=data.get("id", 0), title=data.get("title", ""), - description=data.get("description", ""), + synopsis=data.get("synopsis", ""), genre=data.get("genre", ""), rating=data.get("rating"), + durationMins=data.get("durationMins", 0), + year=data.get("year", 0) ) diff --git a/src/mcp/movie-reviews-mcp/uv.lock b/src/mcp/movie-reviews-mcp/uv.lock index 0ada2fd..df0f880 100644 --- a/src/mcp/movie-reviews-mcp/uv.lock +++ b/src/mcp/movie-reviews-mcp/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -24,21 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] -[[package]] -name = "attractions-mcp" -version = "1.0.0" -source = { virtual = "." } -dependencies = [ - { name = "mcp", extra = ["cli"] }, - { name = "requests" }, -] - -[package.metadata] -requires-dist = [ - { name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }, - { name = "requests" }, -] - [[package]] name = "attrs" version = "25.3.0" @@ -240,6 +225,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "movie-reviews-mcp" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.13.1" }, + { name = "requests" }, +] + [[package]] name = "pydantic" version = "2.11.7" From 7d4d07a8dbfea9b3f6692348fa41356f40d63ab9 Mon Sep 17 00:00:00 2001 From: Justin Chew Date: Wed, 24 Sep 2025 14:58:51 +0100 Subject: [PATCH 20/25] typo type --- src/mcp/movie-reviews-mcp/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py index d5ce01d..24a8f43 100644 --- a/src/mcp/movie-reviews-mcp/main.py +++ b/src/mcp/movie-reviews-mcp/main.py @@ -90,7 +90,7 @@ def search_and_format_movies( @mcp.tool() -def get_movie_reviews_by_title(title: string) -> str: +def get_movie_reviews_by_title(title: str) -> str: """Get reviews for a movie by its title and return formatted results Args: From acdaec269170996e6531500daee32533d42f4103 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:59:59 +0100 Subject: [PATCH 21/25] Update config.py --- src/mcp/movie-reviews-mcp/config.py | 158 ++++++++++++++-------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/config.py b/src/mcp/movie-reviews-mcp/config.py index 7cc1486..9c68265 100644 --- a/src/mcp/movie-reviews-mcp/config.py +++ b/src/mcp/movie-reviews-mcp/config.py @@ -20,71 +20,71 @@ MAX_SEARCH_LIMIT = 100 DEFAULT_RATING_MIN = 3.0 -# Mock attractions data for demonstration +# Mock movies data matching cinema-mcp for demonstration MOCK_MOVIES = [ { "id": 1, - "title": "Inception", - "synopsis": "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.", - "rating": 8.8, - "durationMins": 148, + "title": "Galactic Adventures", + "synopsis": "An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.", + "rating": 8.7, + "durationMins": 142, "genre": "sci-fi" }, { "id": 2, - "title": "The Godfather", - "synopsis": "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", - "rating": 9.2, - "durationMins": 175, - "genre": "drama" + "title": "The Midnight Mystery", + "synopsis": "A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.", + "rating": 8.2, + "durationMins": 118, + "genre": "thriller" }, { "id": 3, - "title": "The Dark Knight", - "synopsis": "When the menace known as the Joker emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham. The Dark Knight must accept one of the greatest psychological and physical tests of his ability to fight injustice.", - "rating": 9.0, - "durationMins": 152, - "genre": "action" + "title": "Laugh Out Loud", + "synopsis": "A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.", + "rating": 7.8, + "durationMins": 95, + "genre": "comedy" }, { "id": 4, - "title": "Pulp Fiction", - "synopsis": "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", - "rating": 8.9, - "durationMins": 154, - "genre": "crime" + "title": "Dragon's Heart", + "synopsis": "An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.", + "rating": 8.5, + "durationMins": 103, + "genre": "animation" }, { "id": 5, - "title": "Forrest Gump", - "synopsis": "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with a low IQ.", - "rating": 8.8, - "durationMins": 142, + "title": "City of Shadows", + "synopsis": "A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.", + "rating": 8.9, + "durationMins": 134, "genre": "drama" }, { "id": 6, - "title": "Interstellar", - "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", - "rating": 8.6, - "durationMins": 169, - "genre": "sci-fi" + "title": "Ocean's Edge", + "synopsis": "A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.", + "rating": 9.1, + "durationMins": 87, + "genre": "documentary" }, { "id": 7, - "title": "Parasite", - "synopsis": "Greed and class discrimination threaten the newly formed symbiotic relationship between the wealthy Park family and the destitute Kim clan.", - "rating": 8.6, - "durationMins": 132, - "genre": "thriller" + "title": "Love in Paris", + "synopsis": "A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.", + "rating": 7.6, + "durationMins": 108, + "genre": "romance" }, { "id": 8, - "title": "Schindler's List", - "synopsis": "In German-occupied Poland during World War II, industrialist Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", - "rating": 8.9, - "durationMins": 195, - "genre": "history" + "title": "Nightmare Manor", + "synopsis": "A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.", + "rating": 7.4, + "durationMins": 106, + "genre": "horror" }, { "id": 9, @@ -189,113 +189,113 @@ "movie_id": 1, "reviewer": "Alice", "rating": 5, - "comment": "A mind-bending masterpiece with stunning visuals and a gripping plot.", - "reviewDate": "2024-03-01" + "comment": "An epic space adventure with stunning visuals and compelling characters. The galaxy scenes are breathtaking!", + "reviewDate": "2024-09-01" }, { "movie_id": 1, "reviewer": "Bob", "rating": 4, - "comment": "Complex and thought-provoking, but a bit hard to follow at times.", - "reviewDate": "2024-03-02" + "comment": "Great sci-fi adventure, though the plot gets a bit convoluted in the middle. Overall very entertaining.", + "reviewDate": "2024-09-02" }, { "movie_id": 2, "reviewer": "Charlie", "rating": 5, - "comment": "An epic tale of family and power. A must-watch classic.", - "reviewDate": "2024-03-03" + "comment": "A masterful thriller that keeps you guessing until the very end. Perfect midnight mystery vibes!", + "reviewDate": "2024-09-03" }, { "movie_id": 2, "reviewer": "Diana", - "rating": 5, - "comment": "Brilliant performances and an unforgettable story.", - "reviewDate": "2024-03-04" + "rating": 4, + "comment": "Excellent detective work and atmospheric tension. The midnight setting adds to the suspense.", + "reviewDate": "2024-09-04" }, { "movie_id": 3, "reviewer": "Eve", - "rating": 5, - "comment": "Heath Ledger's Joker is iconic. A thrilling ride from start to finish.", - "reviewDate": "2024-03-05" + "rating": 4, + "comment": "Hilarious from start to finish! The social media angle is spot-on and very relatable.", + "reviewDate": "2024-09-05" }, { "movie_id": 3, "reviewer": "Frank", - "rating": 4, - "comment": "Great action and depth, though a bit dark for my taste.", - "reviewDate": "2024-03-06" + "rating": 5, + "comment": "Best comedy I've seen this year. The friendship dynamics are heartwarming and funny.", + "reviewDate": "2024-09-06" }, { "movie_id": 4, "reviewer": "Grace", "rating": 5, - "comment": "A wild, stylish film with unforgettable dialogue.", - "reviewDate": "2024-03-07" + "comment": "Beautiful animation and a heartwarming story about dragons and friendship. Perfect for families!", + "reviewDate": "2024-09-07" }, { "movie_id": 4, "reviewer": "Henry", "rating": 4, - "comment": "Unique storytelling and great cast.", - "reviewDate": "2024-03-08" + "comment": "Lovely animated adventure with great voice acting and stunning visuals.", + "reviewDate": "2024-09-08" }, { "movie_id": 5, "reviewer": "Ivy", "rating": 5, - "comment": "Heartwarming and inspirational. Tom Hanks is fantastic.", - "reviewDate": "2024-03-09" + "comment": "A masterful noir drama with incredible atmosphere. The 1940s setting is perfectly captured.", + "reviewDate": "2024-09-09" }, { "movie_id": 5, "reviewer": "Jack", "rating": 4, - "comment": "A touching story with memorable moments.", - "reviewDate": "2024-03-10" + "comment": "Gripping detective story with excellent cinematography and strong performances.", + "reviewDate": "2024-09-10" }, { "movie_id": 6, "reviewer": "Karen", "rating": 5, - "comment": "Visually stunning and emotionally powerful.", - "reviewDate": "2024-03-11" + "comment": "Absolutely breathtaking documentary! The ocean footage is stunning and educational.", + "reviewDate": "2024-09-11" }, { "movie_id": 6, "reviewer": "Leo", - "rating": 4, - "comment": "Ambitious sci-fi with a moving story.", - "reviewDate": "2024-03-12" + "rating": 5, + "comment": "David Attenborough's narration combined with incredible underwater cinematography. A must-see!", + "reviewDate": "2024-09-12" }, { "movie_id": 7, "reviewer": "Mona", - "rating": 5, - "comment": "A brilliant social satire with suspenseful twists.", - "reviewDate": "2024-03-13" + "rating": 4, + "comment": "Charming romantic comedy with beautiful Parisian scenery. Predictable but delightful!", + "reviewDate": "2024-09-13" }, { "movie_id": 7, "reviewer": "Nate", - "rating": 4, - "comment": "Darkly funny and deeply unsettling.", - "reviewDate": "2024-03-14" + "rating": 3, + "comment": "Sweet story but a bit clichéd. The Paris setting makes up for the predictable plot.", + "reviewDate": "2024-09-14" }, { "movie_id": 8, "reviewer": "Olivia", - "rating": 5, - "comment": "A powerful and emotional historical drama.", - "reviewDate": "2024-03-15" + "rating": 4, + "comment": "Genuinely scary horror film with great atmosphere. The old mansion setting is perfect!", + "reviewDate": "2024-09-15" }, { "movie_id": 8, "reviewer": "Paul", - "rating": 5, - "comment": "Heart-wrenching and beautifully acted.", - "reviewDate": "2024-03-16" + "rating": 3, + "comment": "Classic haunted house horror but some jump scares felt forced. Good for horror fans.", + "reviewDate": "2024-09-16" }, { "movie_id": 9, From 75d03c1e203492ca5e8586366a30f0a0a9fbdd7a Mon Sep 17 00:00:00 2001 From: Justin Chew Date: Wed, 24 Sep 2025 15:09:39 +0100 Subject: [PATCH 22/25] fix searching by title --- src/mcp/movie-reviews-mcp/main.py | 3 ++- src/mcp/movie-reviews-mcp/movie_service.py | 3 ++- src/mcp/movie-reviews-mcp/utils.py | 8 +++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/main.py b/src/mcp/movie-reviews-mcp/main.py index 24a8f43..3f24d51 100644 --- a/src/mcp/movie-reviews-mcp/main.py +++ b/src/mcp/movie-reviews-mcp/main.py @@ -40,6 +40,7 @@ def get_movie_details(movie_id: int) -> Dict[str, Any]: @mcp.tool() def search_movies( + title: Optional[str] = None, genre: Optional[str] = None, limit: int = 20 ) -> Dict[str, Any]: @@ -52,7 +53,7 @@ def search_movies( Returns: MoviesList object as dictionary with matching movies """ - return search_movies_data(genre, limit) + return search_movies_data(title, genre, limit) @mcp.tool() def get_random_movie() -> Dict[str, Any]: diff --git a/src/mcp/movie-reviews-mcp/movie_service.py b/src/mcp/movie-reviews-mcp/movie_service.py index bfc88e8..d9991c6 100644 --- a/src/mcp/movie-reviews-mcp/movie_service.py +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -22,6 +22,7 @@ def search_movies_data( + title: str = None, genre: str = None, limit: int = DEFAULT_SEARCH_LIMIT ) -> Dict[str, Any]: @@ -40,7 +41,7 @@ def search_movies_data( elif limit < 1: limit = 1 - data = search_movies(genre, limit) + data = search_movies(title, genre, limit) if not data: return {"error": "No movies found matching the criteria"} diff --git a/src/mcp/movie-reviews-mcp/utils.py b/src/mcp/movie-reviews-mcp/utils.py index 8584518..a272e70 100644 --- a/src/mcp/movie-reviews-mcp/utils.py +++ b/src/mcp/movie-reviews-mcp/utils.py @@ -31,6 +31,7 @@ def get_movie_by_id(movie_id: int) -> Optional[Dict[str, Any]]: def search_movies( + title: Optional[str] = None, genre: Optional[str] = None, limit: int = 20 ) -> Optional[Dict[str, Any]]: @@ -44,7 +45,12 @@ def search_movies( if genre: genre_match = movie["genres"] == genre.lower() - if genre_match: + # Filter by title + title_match = True + if title: + title_match = title.lower() in movie["title"].lower() + + if genre_match and title_match: filtered_movies.append(movie) # Apply limit From 77fc12483c8e20e337b52bbf5d3d510dbdf326b2 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:15:46 +0100 Subject: [PATCH 23/25] Presentation --- demo/demo_template 1.pptx | Bin 2154044 -> 2154018 bytes src/agent/cinema.ipynb | 1716 +++++++++++++++++++++++++++++++++---- 2 files changed, 1539 insertions(+), 177 deletions(-) diff --git a/demo/demo_template 1.pptx b/demo/demo_template 1.pptx index 20f841d62707894e64a700f4e545ca2fda3354b4..ed59ea11d976e442f675a14aa157120c146c1849 100644 GIT binary patch delta 7589 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 BI$i(( delta 7607 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 UlK&SVinv5VS+O?8ps3>i0KZQ=CjbBd diff --git a/src/agent/cinema.ipynb b/src/agent/cinema.ipynb index 3422aac..876384b 100644 --- a/src/agent/cinema.ipynb +++ b/src/agent/cinema.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 24, "id": "a26927c2", "metadata": {}, "outputs": [ @@ -24,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 25, "id": "aba67892", "metadata": {}, "outputs": [ @@ -41,7 +41,7 @@ "output_type": "stream", "text": [ "\u001b[2mUsing Python 3.13.7 environment at: c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\u001b[0m\n", - "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 20ms\u001b[0m\u001b[0m\n" + "\u001b[2mAudited \u001b[1m13 packages\u001b[0m \u001b[2min 23ms\u001b[0m\u001b[0m\n" ] } ], @@ -54,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 26, "id": "b86c12d0", "metadata": {}, "outputs": [ @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 27, "id": "a60c8345", "metadata": {}, "outputs": [ @@ -148,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 28, "id": "57e4f778", "metadata": {}, "outputs": [ @@ -169,7 +169,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 29, "id": "501c5c58", "metadata": {}, "outputs": [ @@ -262,7 +262,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 30, "id": "688da57b", "metadata": {}, "outputs": [ @@ -270,19 +270,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "Initializing cinema and movie reviews agent with MCP tools...\n", - "Error connecting to MCP HTTP servers: unhandled errors in a TaskGroup (1 sub-exception)\n", - "Make sure the MCP servers are running:\n", - "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", - "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n", - "No MCP tools loaded. Make sure the Cinema and Movie Reviews MCP servers are accessible.\n", - "Failed to initialize agent. Check Cinema and Movie Reviews MCP server connections.\n", - "Error connecting to MCP HTTP servers: unhandled errors in a TaskGroup (1 sub-exception)\n", - "Make sure the MCP servers are running:\n", - "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", - "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n", - "No MCP tools loaded. Make sure the Cinema and Movie Reviews MCP servers are accessible.\n", - "Failed to initialize agent. Check Cinema and Movie Reviews MCP server connections.\n" + "Initializing cinema and movie reviews agent with MCP tools...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 14 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "Loaded 14 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "LangChain cinema and movie reviews agent with MCP tools ready!\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_38108\\1472502583.py:67: LangChainDeprecationWarning: Please see the migration guide at: https://python.langchain.com/docs/versions/migrating_memory/\n", + " memory = ConversationBufferMemory(memory_key=\"chat_history\", return_messages=True)\n" ] } ], @@ -304,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 31, "id": "dc7277da", "metadata": {}, "outputs": [ @@ -368,7 +373,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 32, "id": "3c70994e", "metadata": {}, "outputs": [ @@ -376,14 +381,160 @@ "name": "stdout", "output_type": "stream", "text": [ - "Error connecting to MCP HTTP servers: unhandled errors in a TaskGroup (1 sub-exception)\n", - "Make sure the MCP servers are running:\n", - "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", - "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n", - "Failed to connect to MCP HTTP servers\n", - "Make sure to start both MCP servers:\n", - "Cinema: cd src/mcp/cinema-mcp && uv run main.py (port 8010)\n", - "Movie Reviews: cd src/mcp/movie-reviews-mcp && uv run main.py (port 8011)\n" + "Loaded 14 MCP tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "Cinema and Movie Reviews MCP HTTP servers connected successfully!\n", + "Available tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + " - get_current_movies: Get all currently playing movies with their showtimes and availability\n", + "\n", + "Returns:\n", + " List of all movies currently being shown with details including:\n", + " - Movie title, description, and basic info\n", + " - Showtimes and theater room assignments \n", + " - Seat availability and pricing\n", + " - Genre, rating, and duration\n", + " - Cast and director information\n", + "\n", + " - get_movie_details: Get detailed information about a specific movie showing\n", + "\n", + "Args:\n", + " title: Exact movie title\n", + " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " time: Time in HH:MM format (e.g., \"19:30\")\n", + " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", + " \n", + "Returns:\n", + " Detailed movie information including plot, cast, theater details, and availability\n", + " Use search_movies first to find the exact title, date, time, and room values needed\n", + "\n", + " - search_movies: Search for movie presentations with optional filters\n", + "\n", + "Args:\n", + " title: Filter by movie title (partial match, case-insensitive)\n", + " genre: Filter by movie genre (action, comedy, drama, horror, sci-fi, romance, thriller, animation, documentary, family)\n", + " date: Filter by date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " room: Filter by cinema room (theater_a, theater_b, theater_c, imax)\n", + " available_seats_min: Minimum number of available seats required\n", + " limit: Maximum number of results to return (default: 20, max: 100)\n", + " \n", + "Returns:\n", + " Filtered list of movie presentations matching the search criteria\n", + "\n", + " - make_reservation: Create a movie reservation for a specific showing\n", + "\n", + "Args:\n", + " title: Exact movie title (use get_movie_details or search_movies to find exact title)\n", + " date: Date in YYYY-MM-DD format (e.g., \"2025-09-25\")\n", + " time: Time in HH:MM format (e.g., \"19:30\")\n", + " room: Room identifier (e.g., \"theater_a\", \"theater_b\", \"theater_c\", \"imax\")\n", + " seats_count: Number of seats to reserve (must be > 0)\n", + " customer_name: Customer's full name\n", + " customer_email: Customer's email address\n", + " customer_phone: Customer's phone number (optional)\n", + " special_requests: Any special requests or accessibility needs (optional)\n", + " \n", + "Returns:\n", + " Reservation confirmation with booking details and pricing information\n", + " Use the exact title, date, time, and room values from search_movies or get_current_movies results\n", + "\n", + " - get_my_reservations: Get all reservations for a customer\n", + "\n", + "Args:\n", + " customer_email: Customer's email address used for reservations\n", + " \n", + "Returns:\n", + " List of all reservations made by the customer\n", + "\n", + " - cancel_reservation: Cancel a movie reservation\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation to cancel\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " \n", + "Returns:\n", + " Cancellation confirmation and details about freed seats\n", + " Use get_my_reservations first to find the exact details of reservations to cancel\n", + "\n", + " - get_all_reservations: Get all reservations for cinema employee review\n", + "\n", + "Args:\n", + " status_filter: Filter by status (\"confirmed\", \"completed\", \"cancelled\") or None for all active reservations\n", + " \n", + "Returns:\n", + " List of all reservations matching the filter for employee management\n", + " Use this to review current reservations that need attention\n", + "\n", + " - acknowledge_reservation: Acknowledge/complete a reservation (cinema employee function)\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " \n", + "Returns:\n", + " Acknowledgment confirmation - marks reservation as completed\n", + " Use when customer has attended the showing\n", + "\n", + " - revoke_reservation: Revoke/cancel a reservation (cinema employee function)\n", + "\n", + "Args:\n", + " customer_email: Customer's email address\n", + " title: Exact movie title of the reservation to revoke\n", + " date: Date in YYYY-MM-DD format\n", + " time: Time in HH:MM format\n", + " room: Room identifier\n", + " reason: Reason for revocation (optional, for employee records)\n", + " \n", + "Returns:\n", + " Revocation confirmation and details about freed seats\n", + " Use when a reservation needs to be cancelled by cinema staff\n", + "\n", + " - get_movie_details: Get detailed information about a specific movie\n", + "\n", + "Args:\n", + " movie_id: Unique ID of the movie\n", + "\n", + "Returns:\n", + " MovieDetails object as dictionary with movie info, reviews, and related movies\n", + "\n", + " - search_movies: Search for movies with optional filters\n", + "\n", + "Args:\n", + " genre: Genre filter - \"action\", \"comedy\", \"drama\", etc.\n", + " limit: Maximum number of results (1-100, default: 20)\n", + "\n", + "Returns:\n", + " MoviesList object as dictionary with matching movies\n", + "\n", + " - get_random_movie: Get a random movie for inspiration\n", + "\n", + "Args:\n", + " genre: Genre type - \"famous\" for world famous movies, \"india\" for Indian movies\n", + "\n", + "Returns:\n", + " Movie object as dictionary with random movie details\n", + "\n", + " - search_and_format_movies: Search for movies and return formatted results for easy reading\n", + "\n", + "Args:\n", + " genre: Genre filter (e.g., \"action\", \"comedy\", \"drama\")\n", + " limit: Maximum number of results (1-20, default: 10)\n", + "\n", + "Returns:\n", + " Formatted string with movie search results\n", + "\n", + " - get_movie_reviews_by_title: Get reviews for a movie by its title and return formatted results\n", + "\n", + "Args:\n", + " title: Title of the movie\n", + "\n", + "Returns:\n", + " Formatted string with movie reviews\n", + "\n" ] } ], @@ -409,7 +560,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 33, "id": "de62dd60", "metadata": {}, "outputs": [ @@ -418,15 +569,17 @@ "output_type": "stream", "text": [ "🔍 Debugging Cinema and Movie Reviews MCP connections...\n", - "❌ Cinema HTTP connection failed: All connection attempts failed\n", - "❌ Cinema HTTP connection failed: All connection attempts failed\n", - "❌ Movie Reviews HTTP connection failed: All connection attempts failed\n", + "✅ Cinema HTTP connection works: 404\n", + "✅ Cinema HTTP connection works: 404\n", + "✅ Movie Reviews HTTP connection works: 404\n", "✅ MCP client created successfully\n", - "❌ Movie Reviews HTTP connection failed: All connection attempts failed\n", + "✅ Movie Reviews HTTP connection works: 404\n", "✅ MCP client created successfully\n", - "❌ Error getting tools: unhandled errors in a TaskGroup (1 sub-exception)\n", + "✅ Got 14 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", "Full traceback:\n", - "❌ Error getting tools: unhandled errors in a TaskGroup (1 sub-exception)\n", + "✅ Got 14 tools: ['get_current_movies', 'get_movie_details', 'search_movies', 'make_reservation', 'get_my_reservations', 'cancel_reservation', 'get_all_reservations', 'acknowledge_reservation', 'revoke_reservation', 'get_movie_details', 'search_movies', 'get_random_movie', 'search_and_format_movies', 'get_movie_reviews_by_title']\n", + "❌ Error getting tools: 'MultiServerMCPClient' object has no attribute 'close'\n", "Full traceback:\n" ] }, @@ -434,135 +587,11 @@ "name": "stderr", "output_type": "stream", "text": [ - " + Exception Group Traceback (most recent call last):\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_38108\\2853036564.py\", line 42, in debug_mcp_connection\n", - " | tools = await client.get_tools()\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\client.py\", line 157, in get_tools\n", - " | tools_list = await asyncio.gather(*load_mcp_tool_tasks)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\tools.py\", line 188, in load_mcp_tools\n", - " | async with create_session(connection) as tool_session:\n", - " | ~~~~~~~~~~~~~~^^^^^^^^^^^^\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 235, in __aexit__\n", - " | await self.gen.athrow(value)\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\sessions.py\", line 394, in create_session\n", - " | async with _create_streamable_http_session(**params) as session:\n", - " | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 235, in __aexit__\n", - " | await self.gen.athrow(value)\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\langchain_mcp_adapters\\sessions.py\", line 309, in _create_streamable_http_session\n", - " | streamablehttp_client(\n", - " | ~~~~~~~~~~~~~~~~~~~~~^\n", - " | url,\n", - " | ^^^^\n", - " | ...<5 lines>...\n", - " | **kwargs,\n", - " | ^^^^^^^^^\n", - " | ) as (read, write, _),\n", - " | ^\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 235, in __aexit__\n", - " | await self.gen.athrow(value)\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\mcp\\client\\streamable_http.py\", line 478, in streamablehttp_client\n", - " | async with anyio.create_task_group() as tg:\n", - " | ~~~~~~~~~~~~~~~~~~~~~~~^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\anyio\\_backends\\_asyncio.py\", line 781, in __aexit__\n", - " | raise BaseExceptionGroup(\n", - " | \"unhandled errors in a TaskGroup\", self._exceptions\n", - " | ) from None\n", - " | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)\n", - " +-+---------------- 1 ----------------\n", - " | Traceback (most recent call last):\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 101, in map_httpcore_exceptions\n", - " | yield\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 394, in handle_async_request\n", - " | resp = await self._pool.handle_async_request(req)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection_pool.py\", line 256, in handle_async_request\n", - " | raise exc from None\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection_pool.py\", line 236, in handle_async_request\n", - " | response = await connection.handle_async_request(\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | pool_request.request\n", - " | ^^^^^^^^^^^^^^^^^^^^\n", - " | )\n", - " | ^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection.py\", line 101, in handle_async_request\n", - " | raise exc\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection.py\", line 78, in handle_async_request\n", - " | stream = await self._connect(request)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_async\\connection.py\", line 124, in _connect\n", - " | stream = await self._network_backend.connect_tcp(**kwargs)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_backends\\auto.py\", line 31, in connect_tcp\n", - " | return await self._backend.connect_tcp(\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | ...<5 lines>...\n", - " | )\n", - " | ^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_backends\\anyio.py\", line 113, in connect_tcp\n", - " | with map_exceptions(exc_map):\n", - " | ~~~~~~~~~~~~~~^^^^^^^^^\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 162, in __exit__\n", - " | self.gen.throw(value)\n", - " | ~~~~~~~~~~~~~~^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpcore\\_exceptions.py\", line 14, in map_exceptions\n", - " | raise to_exc(exc) from exc\n", - " | httpcore.ConnectError: All connection attempts failed\n", - " | \n", - " | The above exception was the direct cause of the following exception:\n", - " | \n", - " | Traceback (most recent call last):\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\mcp\\client\\streamable_http.py\", line 409, in handle_request_async\n", - " | await self._handle_post_request(ctx)\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\mcp\\client\\streamable_http.py\", line 260, in _handle_post_request\n", - " | async with ctx.client.stream(\n", - " | ~~~~~~~~~~~~~~~~~^\n", - " | \"POST\",\n", - " | ^^^^^^^\n", - " | ...<2 lines>...\n", - " | headers=headers,\n", - " | ^^^^^^^^^^^^^^^^\n", - " | ) as response:\n", - " | ^\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 214, in __aenter__\n", - " | return await anext(self.gen)\n", - " | ^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1583, in stream\n", - " | response = await self.send(\n", - " | ^^^^^^^^^^^^^^^^\n", - " | ...<4 lines>...\n", - " | )\n", - " | ^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1629, in send\n", - " | response = await self._send_handling_auth(\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | ...<4 lines>...\n", - " | )\n", - " | ^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1657, in _send_handling_auth\n", - " | response = await self._send_handling_redirects(\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | ...<3 lines>...\n", - " | )\n", - " | ^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1694, in _send_handling_redirects\n", - " | response = await self._send_single_request(request)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_client.py\", line 1730, in _send_single_request\n", - " | response = await transport.handle_async_request(request)\n", - " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 393, in handle_async_request\n", - " | with map_httpcore_exceptions():\n", - " | ~~~~~~~~~~~~~~~~~~~~~~~^^\n", - " | File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\contextlib.py\", line 162, in __exit__\n", - " | self.gen.throw(value)\n", - " | ~~~~~~~~~~~~~~^^^^^^^\n", - " | File \"c:\\Users\\AndrzejPytel\\source\\Hackathon-2025-AP-Fork\\.venv\\Lib\\site-packages\\httpx\\_transports\\default.py\", line 118, in map_httpcore_exceptions\n", - " | raise mapped_exc(message) from exc\n", - " | httpx.ConnectError: All connection attempts failed\n", - " +------------------------------------\n" + "Traceback (most recent call last):\n", + " File \"C:\\Users\\AndrzejPytel\\AppData\\Local\\Temp\\ipykernel_38108\\2853036564.py\", line 46, in debug_mcp_connection\n", + " await client.close()\n", + " ^^^^^^^^^^^^\n", + "AttributeError: 'MultiServerMCPClient' object has no attribute 'close'\n" ] } ], @@ -631,7 +660,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 34, "id": "1551f227", "metadata": {}, "outputs": [ @@ -641,7 +670,614 @@ "text": [ "🎬 User: What movies are currently playing?\n", "🤖 Assistant:\n", - "Agent not initialized. Please run the initialization cell first.\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\n", + "\n", + "==================================================\n", + "\n", + "\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema. Want more details or to book tickets? I can help with either.\n", + "\n", + "1) Galactic Adventures — Theater A — 2025-09-25 at 14:30\n", + "- Genre: Science Fiction | Rating: PG-13 | Duration: 142 min\n", + "- Price: $12.50 per seat | Seats left: 105/150\n", + "- Description: A space epic about a crew racing through distant galaxies to save humanity from an alien threat.\n", + "- Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + "- Director: Sarah Johnson\n", + "\n", + "2) The Midnight Mystery — Theater B — 2025-09-25 at 19:15\n", + "- Genre: Thriller | Rating: R | Duration: 118 min\n", + "- Price: $14.00 | Seats left: 75/200\n", + "- Description: A small-town sheriff investigates mysterious disappearances that all point to midnight.\n", + "- Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + "- Director: Michael Davis\n", + "\n", + "3) Laugh Out Loud — Theater A — 2025-09-25 at 21:45\n", + "- Genre: Comedy | Rating: PG-13 | Duration: 95 min\n", + "- Price: $12.50 | Seats left: 61/150\n", + "- Description: Three friends become viral internet sensations and navigate the chaos of fame.\n", + "- Cast: Tom Martinez, Sarah Kim, David Brown\n", + "- Director: Jennifer Lee\n", + "\n", + "4) Dragon's Heart — Theater C — 2025-09-26 at 10:00\n", + "- Genre: Animation | Rating: G | Duration: 103 min\n", + "- Price: $10.00 | Seats left: 77/100\n", + "- Description: A young girl talks with dragons to save her village from an ancient curse.\n", + "- Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + "- Director: Animation Studios Inc.\n", + "\n", + "5) City of Shadows — Theater B — 2025-09-26 at 16:20\n", + "- Genre: Drama | Rating: R | Duration: 134 min\n", + "- Price: $14.00 | Seats left: 44/200\n", + "- Description: A noir about a detective uncovering corruption in 1940s NYC.\n", + "- Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + "- Director: Vincent Romano\n", + "\n", + "6) Ocean's Edge — IMAX Theater — 2025-09-26 at 20:00\n", + "- Genre: Documentary | Rating: G | Duration: 87 min\n", + "- Price: $18.00 | Seats left: 222/300\n", + "- Description: IMAX documentary exploring the deepest oceans and their creatures.\n", + "- Cast: Narrator: David Attenborough\n", + "- Director: Ocean Explorer Films\n", + "\n", + "7) Love in Paris — Theater C — 2025-09-27 at 15:45\n", + "- Genre: Romance | Rating: PG | Duration: 108 min\n", + "- Price: $11.50 | Seats left: 33/100\n", + "- Description: An American tourist finds love in Paris with a local café owner.\n", + "- Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + "- Director: Claire Dubois\n", + "\n", + "8) Nightmare Manor — Theater A — 2025-09-27 at 22:30\n", + "- Genre: Horror | Rating: R | Duration: 106 min\n", + "- Price: $13.00 | Seats left: 58/150\n", + "- Description: A family inherits a haunted mansion with spirits of previous owners.\n", + "- Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + "- Director: Horror Productions\n", + "\n", + "Would you like me to:\n", + "- fetch more detailed information or reviews for any of these titles\n", + "- check real-time seat availability for a specific showing\n", + "- start a reservation (I’ll need your name, email, and how many seats you want)\n", "\n", "==================================================\n", "\n" @@ -662,7 +1298,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "9295d748", "metadata": {}, "outputs": [ @@ -674,14 +1310,740 @@ "I can help you find movies, check reviews, get ratings, check showtimes, and make reservations.\n", "Type 'exit' to quit. Press Enter on an empty line to skip.\n", "\n", - "🎬 User: Hello\n", + "🎬 User: What are the reviews for The Midnight Mystery\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: What are the reviews for The Midnight Mystery\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_movie_reviews_by_title` with `{'title': 'The Midnight Mystery'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_movie_reviews_by_title` with `{'title': 'The Midnight Mystery'}`\n", + "\n", + "\n", + "\u001b[0mError processing request: Error executing tool get_movie_reviews_by_title: search_movies() got an unexpected keyword argument 'title'\n", + "Error processing request: Error executing tool get_movie_reviews_by_title: search_movies() got an unexpected keyword argument 'title'\n", + "🎬 User: what movies are playing in cinema currently\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: what movies are playing in cinema currently\n", + "🤖 Assistant:\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `get_current_movies` with `{}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[36;1m\u001b[1;3m{\n", + " \"cinema_name\": \"MovieMagic Cinema\",\n", + " \"total_movies\": 8,\n", + " \"movies\": [\n", + " {\n", + " \"title\": \"Galactic Adventures\",\n", + " \"description\": \"An epic space adventure following a crew of explorers as they journey through distant galaxies to save humanity from an alien threat.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"14:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 105,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 142,\n", + " \"genre\": \"Science Fiction\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Sarah Johnson\",\n", + " \"cast\": [\n", + " \"Alex Thompson\",\n", + " \"Maria Rodriguez\",\n", + " \"James Chen\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 30.0\n", + " },\n", + " {\n", + " \"title\": \"The Midnight Mystery\",\n", + " \"description\": \"A thrilling detective story about a small town sheriff investigating a series of mysterious disappearances that all happen at midnight.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"19:15\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 75,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 118,\n", + " \"genre\": \"Thriller\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Michael Davis\",\n", + " \"cast\": [\n", + " \"Emma Wilson\",\n", + " \"Robert Garcia\",\n", + " \"Lisa Park\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 62.5\n", + " },\n", + " {\n", + " \"title\": \"Laugh Out Loud\",\n", + " \"description\": \"A hilarious comedy about three friends who accidentally become viral internet sensations and must navigate their newfound fame.\",\n", + " \"date\": \"2025-09-25\",\n", + " \"time\": \"21:45\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 61,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 95,\n", + " \"genre\": \"Comedy\",\n", + " \"rating\": \"PG-13\",\n", + " \"price_per_seat\": 12.5,\n", + " \"director\": \"Jennifer Lee\",\n", + " \"cast\": [\n", + " \"Tom Martinez\",\n", + " \"Sarah Kim\",\n", + " \"David Brown\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 59.3\n", + " },\n", + " {\n", + " \"title\": \"Dragon's Heart\",\n", + " \"description\": \"An animated fantasy adventure about a young girl who discovers she can communicate with dragons and must save her village from an ancient curse.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"10:00\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 77,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 103,\n", + " \"genre\": \"Animation\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 10.0,\n", + " \"director\": \"Animation Studios Inc.\",\n", + " \"cast\": [\n", + " \"Voice Cast: Amy Johnson\",\n", + " \"Mark Stevens\",\n", + " \"Luna Rodriguez\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 23.0\n", + " },\n", + " {\n", + " \"title\": \"City of Shadows\",\n", + " \"description\": \"A noir drama set in 1940s New York following a detective uncovering corruption in the police department while solving a murder case.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"16:20\",\n", + " \"room\": \"Theater B\",\n", + " \"seats_remaining\": 44,\n", + " \"seats_total\": 200,\n", + " \"duration_minutes\": 134,\n", + " \"genre\": \"Drama\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 14.0,\n", + " \"director\": \"Vincent Romano\",\n", + " \"cast\": [\n", + " \"Antonio Silva\",\n", + " \"Catherine Moore\",\n", + " \"Frank Williams\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 78.0\n", + " },\n", + " {\n", + " \"title\": \"Ocean's Edge\",\n", + " \"description\": \"A spectacular IMAX documentary exploring the deepest parts of our oceans and the incredible creatures that call them home.\",\n", + " \"date\": \"2025-09-26\",\n", + " \"time\": \"20:00\",\n", + " \"room\": \"IMAX Theater\",\n", + " \"seats_remaining\": 222,\n", + " \"seats_total\": 300,\n", + " \"duration_minutes\": 87,\n", + " \"genre\": \"Documentary\",\n", + " \"rating\": \"G\",\n", + " \"price_per_seat\": 18.0,\n", + " \"director\": \"Ocean Explorer Films\",\n", + " \"cast\": [\n", + " \"Narrator: David Attenborough\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 26.0\n", + " },\n", + " {\n", + " \"title\": \"Love in Paris\",\n", + " \"description\": \"A romantic comedy about an American tourist who gets lost in Paris and finds love with a local café owner who helps her navigate the city.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"15:45\",\n", + " \"room\": \"Theater C\",\n", + " \"seats_remaining\": 33,\n", + " \"seats_total\": 100,\n", + " \"duration_minutes\": 108,\n", + " \"genre\": \"Romance\",\n", + " \"rating\": \"PG\",\n", + " \"price_per_seat\": 11.5,\n", + " \"director\": \"Claire Dubois\",\n", + " \"cast\": [\n", + " \"Sophie Martin\",\n", + " \"Jean-Luc Moreau\",\n", + " \"Isabella Jones\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 67.0\n", + " },\n", + " {\n", + " \"title\": \"Nightmare Manor\",\n", + " \"description\": \"A spine-chilling horror film about a family that inherits an old mansion, only to discover it's haunted by the spirits of its previous owners.\",\n", + " \"date\": \"2025-09-27\",\n", + " \"time\": \"22:30\",\n", + " \"room\": \"Theater A\",\n", + " \"seats_remaining\": 58,\n", + " \"seats_total\": 150,\n", + " \"duration_minutes\": 106,\n", + " \"genre\": \"Horror\",\n", + " \"rating\": \"R\",\n", + " \"price_per_seat\": 13.0,\n", + " \"director\": \"Horror Productions\",\n", + " \"cast\": [\n", + " \"Scary Actor 1\",\n", + " \"Scary Actor 2\",\n", + " \"Scary Actor 3\"\n", + " ],\n", + " \"is_sold_out\": false,\n", + " \"occupancy_percentage\": 61.3\n", + " }\n", + " ]\n", + "}\u001b[0m\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\n", + "\u001b[32;1m\u001b[1;3mHere are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Here are the movies currently playing at MovieMagic Cinema:\n", + "\n", + "- Galactic Adventures — Theater A\n", + " - Date/Time: 2025-09-25 at 14:30\n", + " - Duration: 142 min | Genre: Science Fiction | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 105/150\n", + " - Cast: Alex Thompson, Maria Rodriguez, James Chen\n", + " - Director: Sarah Johnson\n", + "\n", + "- The Midnight Mystery — Theater B\n", + " - Date/Time: 2025-09-25 at 19:15\n", + " - Duration: 118 min | Genre: Thriller | Rating: R\n", + " - Price: $14.00 | Seats remaining: 75/200\n", + " - Cast: Emma Wilson, Robert Garcia, Lisa Park\n", + " - Director: Michael Davis\n", + "\n", + "- Laugh Out Loud — Theater A\n", + " - Date/Time: 2025-09-25 at 21:45\n", + " - Duration: 95 min | Genre: Comedy | Rating: PG-13\n", + " - Price: $12.50 | Seats remaining: 61/150\n", + " - Cast: Tom Martinez, Sarah Kim, David Brown\n", + " - Director: Jennifer Lee\n", + "\n", + "- Dragon's Heart — Theater C\n", + " - Date/Time: 2025-09-26 at 10:00\n", + " - Duration: 103 min | Genre: Animation | Rating: G\n", + " - Price: $10.00 | Seats remaining: 77/100\n", + " - Cast: Voice Cast: Amy Johnson, Mark Stevens, Luna Rodriguez\n", + " - Director: Animation Studios Inc.\n", + "\n", + "- City of Shadows — Theater B\n", + " - Date/Time: 2025-09-26 at 16:20\n", + " - Duration: 134 min | Genre: Drama | Rating: R\n", + " - Price: $14.00 | Seats remaining: 44/200\n", + " - Cast: Antonio Silva, Catherine Moore, Frank Williams\n", + " - Director: Vincent Romano\n", + "\n", + "- Ocean's Edge — IMAX Theater\n", + " - Date/Time: 2025-09-26 at 20:00\n", + " - Duration: 87 min | Genre: Documentary | Rating: G\n", + " - Price: $18.00 | Seats remaining: 222/300\n", + " - Cast: Narrator: David Attenborough\n", + " - Director: Ocean Explorer Films\n", + "\n", + "- Love in Paris — Theater C\n", + " - Date/Time: 2025-09-27 at 15:45\n", + " - Duration: 108 min | Genre: Romance | Rating: PG\n", + " - Price: $11.50 | Seats remaining: 33/100\n", + " - Cast: Sophie Martin, Jean-Luc Moreau, Isabella Jones\n", + " - Director: Claire Dubois\n", + "\n", + "- Nightmare Manor — Theater A\n", + " - Date/Time: 2025-09-27 at 22:30\n", + " - Duration: 106 min | Genre: Horror | Rating: R\n", + " - Price: $13.00 | Seats remaining: 58/150\n", + " - Cast: Scary Actor 1, Scary Actor 2, Scary Actor 3\n", + " - Director: Horror Productions\n", + "\n", + "Would you like:\n", + "- more details or reviews for any of these titles?\n", + "- to check real-time seat availability for a specific showing?\n", + "- to start a reservation? If so, I’ll need your name, email, and how many seats you want.\n", + "🎬 User: can I make reservation for Laugh Out Loud\n", "🤖 Assistant:\n", - "Agent not initialized. Please run the initialization cell first.\n", - "🎬 User: Hello\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "🎬 User: can I make reservation for Laugh Out Loud\n", "🤖 Assistant:\n", - "Agent not initialized. Please run the initialization cell first.\n", - "🎬 Thanks for using MovieMagic Cinema & Reviews! Goodbye!\n", - "🎬 Thanks for using MovieMagic Cinema & Reviews! Goodbye!\n" + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mFantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Fantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\n", + "\u001b[32;1m\u001b[1;3mFantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "Fantastic choice! I can help you reserve Laugh Out Loud.\n", + "\n", + "Showing details:\n", + "- Movie: Laugh Out Loud\n", + "- Theater: A\n", + "- Date: 2025-09-25\n", + "- Time: 21:45\n", + "- Duration: 95 minutes\n", + "- Genre: Comedy | Rating: PG-13\n", + "- Price per seat: $12.50\n", + "- Seats remaining: 61 of 150\n", + "\n", + "To book, I’ll need:\n", + "- Full name (as it should appear on the reservation)\n", + "- Email address (for the confirmation)\n", + "- Number of seats you want (1 up to 61 currently available)\n", + "\n", + "Optional:\n", + "- Phone number (for contact)\n", + "- Any special requests (e.g., accessibility needs, seating preferences)\n", + "\n", + "Once you share these, I’ll place the reservation and send you the confirmation with the booking details and total price.\n" ] } ], @@ -722,7 +2084,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "e70dad2d", "metadata": {}, "outputs": [ From 032222c67adc053e09dda06b77baaed321076455 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:48:26 +0100 Subject: [PATCH 24/25] Update movie_service.py --- src/mcp/movie-reviews-mcp/movie_service.py | 32 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/mcp/movie-reviews-mcp/movie_service.py b/src/mcp/movie-reviews-mcp/movie_service.py index d9991c6..5113f21 100644 --- a/src/mcp/movie-reviews-mcp/movie_service.py +++ b/src/mcp/movie-reviews-mcp/movie_service.py @@ -176,9 +176,37 @@ def get_movie_reviews_data(movie_id: int) -> MovieReviewList: movie = get_movie_by_id(movie_id) - reviews = [review for review in MOCK_REVIEWS if review.movie_id == movie_id] + # Filter reviews by movie_id (MOCK_REVIEWS contains dictionaries) + review_dicts = [review for review in MOCK_REVIEWS if review["movie_id"] == movie_id] + + # Convert dictionary reviews to MovieReview objects + reviews = [] + for review_dict in review_dicts: + review_date = datetime.strptime(review_dict["reviewDate"], "%Y-%m-%d") + review = MovieReview( + movie_id=review_dict["movie_id"], + rating=review_dict["rating"], + comment=review_dict["comment"], + reviewer=review_dict["reviewer"], + reviewDate=review_date + ) + reviews.append(review) + + # Create movie object if movie data exists + movie_obj = None + if movie: + movie_obj = Movie( + id=movie["id"], + title=movie["title"], + synopsis=movie["synopsis"], + rating=movie["rating"], + durationMins=movie["durationMins"], + year=2024, # Default year since not in movie data + genre=movie["genre"] + ) + review_list = MovieReviewList( - movie=movie, + movie=movie_obj, total_count=len(reviews), reviews=reviews, avg_rating=(sum(r.rating for r in reviews) / len(reviews)) if reviews else None From eb7c4d473ab1f8fcb2db7cc4767ec2f65b4641f4 Mon Sep 17 00:00:00 2001 From: Andrzej Pytel <76943249+PytelAndrzej@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:04:28 +0100 Subject: [PATCH 25/25] Update demo_template 1.pptx --- demo/demo_template 1.pptx | Bin 2154018 -> 2572045 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/demo/demo_template 1.pptx b/demo/demo_template 1.pptx index ed59ea11d976e442f675a14aa157120c146c1849..b44b99393e98d3edea842461842f9644e2483377 100644 GIT binary patch delta 424554 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°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�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 H5q|t1*)W5}