diff --git a/.github/actions/repo-memory-query/README.md b/.github/actions/repo-memory-query/README.md new file mode 100644 index 0000000000..564f1d6c96 --- /dev/null +++ b/.github/actions/repo-memory-query/README.md @@ -0,0 +1,62 @@ +# Repo Memory Query + +A read-only search of the [Repo Memory](https://github.com/redis-learn/redis-repo-memory) store from a +description of work you are about to do. Use it to find related prior PRs, commits and issues, with +their full squash or commit messages, before any PR exists. + +It is separate from the `redis-learn/redis-repo-memory` action on purpose. That action runs on every +push and PR and writes to the store. This one only reads, runs only when someone starts it, and can +change or go away without affecting the memory comments. + +It never creates the index, never writes a key, and never posts to a PR or commit. It uses the same +`MEMORY_REDIS_URL` and `OPENAI_API_KEY` secrets as `repo-memory.yml`. + +## Run it + +```sh +gh workflow run repo-memory-query.yml \ + -f query="Move the job-queue use case from JedisPool to RedisClient" \ + -f files="content/develop/use-cases/job-queue/java-jedis/RedisJobQueue.java" +``` + +The workflow file must be on the default branch for `gh workflow run` to find it. Results appear in +the run's step summary as a list. The full text of each result is on one log line that starts with +`REPO_MEMORY_QUERY_RESULT_JSON`, followed by JSON, so a script can read it from +`gh run view --log`. + +## Inputs + +| Input | Default | Meaning | +| --- | --- | --- | +| `redis-url` | required | Redis URL of the memory store | +| `openai-api-key` | required | Embeds the query with `text-embedding-3-small` | +| `query` | required | The planned work | +| `query-files` | none | Files you plan to change | +| `query-limit` | `5` | Maximum results | +| `query-max-distance` | `0.60` | Cosine distance cutoff, as in the memory action | +| `github-token` | `github.token` | Reads the full message behind each result | + +Only items from the repository the workflow runs in are returned. + +## Visibility + +Workflow logs of a public repository are public. The query text, and the results, are printed in the +log, so don't put anything confidential in a query. The results are titles and messages of this +repository's own public PRs and commits. + +## Compatibility with the memory action + +`query.js` reads what `redis-learn/redis-repo-memory` writes: the `repo_memory_idx` index, `memory:` +hashes, and a cosine `embedding` field from `text-embedding-3-small`. If the memory action changes +that layout, update `query.js` and the seed in `test.js` to match. + +## Test + +The test needs an empty Redis 8 (Search is built in) and uses a mock embedding, so it needs no +OpenAI key or network access. It refuses to run against a database that has any keys. + +```sh +cd .github/actions/repo-memory-query +npm ci +REPO_MEMORY_TEST_REDIS_URL=redis://localhost:6379 node test.js +``` diff --git a/.github/actions/repo-memory-query/action.yml b/.github/actions/repo-memory-query/action.yml new file mode 100644 index 0000000000..3a0507d74b --- /dev/null +++ b/.github/actions/repo-memory-query/action.yml @@ -0,0 +1,60 @@ +name: 'Redis Repo Memory Query' +description: > + Read-only search of an existing Redis Repo Memory store from a description of planned work. + Never writes to Redis and never posts to a pull request or commit. + +inputs: + redis-url: + description: > + Redis connection URL of the Repo Memory store (the same value the + redis-learn/redis-repo-memory action uses), e.g. redis://default:@: + required: true + openai-api-key: + description: OpenAI API key used to embed the query (text-embedding-3-small). + required: true + query: + description: Description of the work you are about to do. + required: true + query-files: + description: Files you plan to change, separated by spaces, commas or newlines. + required: false + default: '' + query-limit: + description: Maximum number of results. + required: false + default: '5' + query-max-distance: + description: Maximum cosine distance (0-2, lower is more similar) for a result. + required: false + default: '0.60' + github-token: + description: > + Token used to read the full squash or commit message behind each result. + The default github.token is enough for a public repository. + required: false + default: ${{ github.token }} + +runs: + using: 'composite' + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + shell: bash + working-directory: ${{ github.action_path }} + run: npm ci --omit=dev + + - name: Query memory + shell: bash + env: + REDIS_URL: ${{ inputs.redis-url }} + OPENAI_API_KEY: ${{ inputs.openai-api-key }} + GITHUB_TOKEN: ${{ inputs.github-token }} + QUERY_TEXT: ${{ inputs.query }} + QUERY_FILES: ${{ inputs.query-files }} + QUERY_LIMIT: ${{ inputs.query-limit }} + QUERY_MAX_DISTANCE: ${{ inputs.query-max-distance }} + run: node "${{ github.action_path }}/query.js" diff --git a/.github/actions/repo-memory-query/package-lock.json b/.github/actions/repo-memory-query/package-lock.json new file mode 100644 index 0000000000..01dd082b44 --- /dev/null +++ b/.github/actions/repo-memory-query/package-lock.json @@ -0,0 +1,554 @@ +{ + "name": "repo-memory-query", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "repo-memory-query", + "version": "1.0.0", + "dependencies": { + "openai": "^4.77.0", + "redis": "^4.7.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "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/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "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/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "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/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.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/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/.github/actions/repo-memory-query/package.json b/.github/actions/repo-memory-query/package.json new file mode 100644 index 0000000000..615fbac6e6 --- /dev/null +++ b/.github/actions/repo-memory-query/package.json @@ -0,0 +1,13 @@ +{ + "name": "repo-memory-query", + "version": "1.0.0", + "private": true, + "description": "Read-only query of a Redis Repo Memory store", + "engines": { + "node": ">=18" + }, + "dependencies": { + "openai": "^4.77.0", + "redis": "^4.7.0" + } +} diff --git a/.github/actions/repo-memory-query/query.js b/.github/actions/repo-memory-query/query.js new file mode 100644 index 0000000000..9bbb3d45f4 --- /dev/null +++ b/.github/actions/repo-memory-query/query.js @@ -0,0 +1,185 @@ +/** + * query.js + * + * Searches a Repo Memory store (written by redis-learn/redis-repo-memory) for items related to a + * description of planned work, and prints the matches. Read-only: it never creates the index, + * never writes a key, and never posts to GitHub. + * + * It must stay compatible with how that action stores items: index `repo_memory_idx`, hashes + * under `memory:`, a COSINE `embedding` field from text-embedding-3-small. + * + * Environment: + * REDIS_URL, OPENAI_API_KEY Same values the memory action uses. + * QUERY_TEXT Required. The planned work. + * QUERY_FILES Optional. Files to be changed. + * QUERY_LIMIT, QUERY_MAX_DISTANCE + * GITHUB_TOKEN Optional. Without it results carry no full text. + * GITHUB_REPOSITORY Only items from this repository are returned. + * REPO_MEMORY_EMBED_PROVIDER=mock Tests only: deterministic local embedding, no API call. + */ +'use strict'; + +const fs = require('fs'); +const { createClient } = require('redis'); + +const INDEX_NAME = 'repo_memory_idx'; +const MODEL = 'text-embedding-3-small'; +const DIM = 1536; +const DEFAULT_LIMIT = 5; +const DEFAULT_MAX_DISTANCE = 0.60; +const FULL_TEXT_MAX = 8000; + +const isMock = () => process.env.REPO_MEMORY_EMBED_PROVIDER === 'mock'; + +function mockEmbedding(text) { + const v = new Float32Array(DIM); + for (const word of text.toLowerCase().match(/[a-z0-9_.]+/g) || []) { + let h = 2166136261; + for (let i = 0; i < word.length; i++) h = Math.imul(h ^ word.charCodeAt(i), 16777619) >>> 0; + v[h % DIM] += 1; + } + const norm = Math.sqrt(v.reduce((s, x) => s + x * x, 0)) || 1; + return Array.from(v, x => x / norm); +} + +async function embed(text, apiKey) { + if (isMock()) return mockEmbedding(text); + const OpenAI = require('openai'); + const res = await new OpenAI({ apiKey }).embeddings.create({ model: MODEL, input: text }); + return res.data[0].embedding; +} + +async function githubGet(path, token) { + const res = await fetch(`https://api.github.com${path}`, { + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + if (!res.ok) console.error(`GitHub API ${res.status} for ${path}`); + return res.ok ? res.json() : null; +} + +// Stored summaries are cut to 500 characters, so read the full text at the source: the squash +// commit message for a merged PR, the commit message for a push, the body otherwise. Returns {} +// when the lookup fails; the stored summary is still reported. +async function fullTextFor(item, token) { + const m = (item.sourceUrl || '').match(/github\.com\/([^/]+\/[^/]+)\/(pull|commit|issues)\/([^/?#]+)/); + if (!m || !token) return {}; + const [, repo, kind, id] = m; + try { + if (kind === 'commit') { + const c = await githubGet(`/repos/${repo}/commits/${id}`, token); + return c ? { fullText: c.commit.message.slice(0, FULL_TEXT_MAX), state: 'commit' } : {}; + } + if (kind === 'pull') { + const p = await githubGet(`/repos/${repo}/pulls/${id}`, token); + if (!p) return {}; + if (p.merged && p.merge_commit_sha) { + const c = await githubGet(`/repos/${repo}/commits/${p.merge_commit_sha}`, token); + const msg = c && !/^Merge pull request/.test(c.commit.message) ? c.commit.message : (p.body || ''); + return { fullText: msg.slice(0, FULL_TEXT_MAX), state: 'merged', mergedAt: p.merged_at }; + } + return { fullText: (p.body || '').slice(0, FULL_TEXT_MAX), state: p.state, mergedAt: null }; + } + const i = await githubGet(`/repos/${repo}/issues/${id}`, token); + return i ? { fullText: (i.body || '').slice(0, FULL_TEXT_MAX), state: i.state } : {}; + } catch { + return {}; + } +} + +// Escape every character that is special in a TAG filter, such as the "/" and "-" in a repo name. +const escapeTag = s => s.replace(/[^A-Za-z0-9_]/g, c => '\\' + c); + +function report(query, files, results) { + // One line that `gh run view --log` output can be grepped for and parsed. + console.log('REPO_MEMORY_QUERY_RESULT_JSON ' + JSON.stringify({ query, files, results })); + const lines = ['## 🧠 Redis Memory query', '']; + if (results.length === 0) { + lines.push('No related prior context found.'); + } else { + for (const r of results) { + const link = r.sourceUrl ? `[${r.title}](${r.sourceUrl})` : r.title; + lines.push(`- ${link} (distance ${r.score.toFixed(2)}${r.state ? `, ${r.state}` : ''})`); + } + } + const out = lines.join('\n') + '\n'; + if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, out); + else process.stdout.write(out); +} + +async function main() { + const { REDIS_URL, OPENAI_API_KEY, GITHUB_TOKEN, GITHUB_REPOSITORY } = process.env; + const query = (process.env.QUERY_TEXT || '').trim(); + if (!REDIS_URL) throw new Error('REDIS_URL is not set'); + if (!OPENAI_API_KEY && !isMock()) throw new Error('OPENAI_API_KEY is not set'); + if (!query) throw new Error('QUERY_TEXT is empty'); + if (!GITHUB_REPOSITORY) throw new Error('GITHUB_REPOSITORY is not set'); + + const files = (process.env.QUERY_FILES || '').split(/[\s,]+/).filter(Boolean); + const limit = Math.max(1, parseInt(process.env.QUERY_LIMIT || '', 10) || DEFAULT_LIMIT); + const maxDistance = parseFloat(process.env.QUERY_MAX_DISTANCE || '') || DEFAULT_MAX_DISTANCE; + + // Same text shape the memory action embeds for a push: title, body, then the file list. + const title = query.split('\n')[0].slice(0, 200); + const searchText = [title, query, files.join(' ')].join('\n').slice(0, 4000); + + const client = createClient({ url: REDIS_URL }); + client.on('error', err => console.error('Redis error:', err.message)); + await client.connect(); + try { + const exists = await client.ft.info(INDEX_NAME).then(() => true).catch(() => false); + if (!exists) { + console.log(`index ${INDEX_NAME} not found; nothing to search`); + report(query, files, []); + return; + } + + const blob = Buffer.from(new Float32Array(await embed(searchText, OPENAI_API_KEY)).buffer); + // Filter to this repository inside the search, so other repositories' items cannot use up the + // neighbors, and set LIMIT because FT.SEARCH returns only 10 rows by default. + const k = Math.max(10, limit * 2); + const found = await client.ft.search( + INDEX_NAME, + `@repo:{${escapeTag(GITHUB_REPOSITORY)}}=>[KNN ${k} @embedding $BLOB AS score]`, + { + LIMIT: { from: 0, size: k }, + PARAMS: { BLOB: blob }, + RETURN: ['type', 'title', 'body_summary', 'source_url', 'created_at', 'score', 'repo'], + SORTBY: { BY: 'score', DIRECTION: 'ASC' }, + DIALECT: 2, + } + ); + + const related = found.documents + .filter(d => parseFloat(d.value.score ?? '2') < maxDistance && d.value.repo === GITHUB_REPOSITORY) + .slice(0, limit); + + const results = []; + for (const d of related) { + const item = { + type: d.value.type, + title: d.value.title, + bodySummary: d.value.body_summary, + sourceUrl: d.value.source_url, + createdAt: d.value.created_at, + score: parseFloat(d.value.score ?? '2'), + }; + results.push({ ...item, ...(await fullTextFor(item, GITHUB_TOKEN)) }); + } + report(query, files, results); + } finally { + await client.quit(); + } +} + +if (require.main === module) { + main().catch(err => { + console.error('repo-memory-query failed:', err.message); + process.exit(1); + }); +} + +module.exports = { mockEmbedding }; diff --git a/.github/actions/repo-memory-query/test.js b/.github/actions/repo-memory-query/test.js new file mode 100644 index 0000000000..1d1c732253 --- /dev/null +++ b/.github/actions/repo-memory-query/test.js @@ -0,0 +1,159 @@ +/** + * test.js + * + * Offline test of query.js with the mock embedding provider (no OpenAI key, no GitHub calls). + * + * REPO_MEMORY_TEST_REDIS_URL=redis://localhost:6395 node test.js + * + * Needs a Redis with the Search module (Redis 8+ has it built in). It refuses to run unless the + * database is EMPTY and deletes everything it creates, so it cannot touch real memory data. + * + * The seeded index and hashes copy the layout written by redis-learn/redis-repo-memory + * (scripts/retrieve_memory.js). If that layout changes, update the seed here and in query.js. + */ +'use strict'; + +const assert = require('assert'); +const { execFileSync } = require('child_process'); +const path = require('path'); +const { createClient, SchemaFieldTypes, VectorAlgorithms } = require('redis'); + +const url = process.env.REPO_MEMORY_TEST_REDIS_URL; +if (!url) { + console.error('Set REPO_MEMORY_TEST_REDIS_URL to an EMPTY Redis (with Search) to run this test.'); + process.exit(2); +} + +const REPO = 'test/repo'; +const ITEMS = [ + ['1', 'Move the job-queue Jedis use case from JedisPool to RedisClient and close every pipeline'], + ['2', 'Bump Lettuce pins in the harness poms to 7.9.0 and update install snippets'], + ['3', 'Raise the redis-rs response timeout for blocking commands in the Rust demos'], + ['4', 'Rewrite the sorted set tutorial to use ZRANGE with BYSCORE and REV'], +]; + +function query(env) { + const out = execFileSync('node', [path.join(__dirname, 'query.js')], { + env: { + PATH: process.env.PATH, + REDIS_URL: url, + REPO_MEMORY_EMBED_PROVIDER: 'mock', + GITHUB_REPOSITORY: REPO, + ...env, + }, + encoding: 'utf8', + }); + const line = out.split('\n').find(l => l.startsWith('REPO_MEMORY_QUERY_RESULT_JSON ')); + assert.ok(line, `result line printed; output was: ${out}`); + return JSON.parse(line.slice('REPO_MEMORY_QUERY_RESULT_JSON '.length)); +} + +async function main() { + const { mockEmbedding } = require('./query.js'); + const client = createClient({ url }); + await client.connect(); + try { + assert.strictEqual(await client.dbSize(), 0, 'refusing to run: the test Redis is not empty'); + + // No index yet: nothing to find, and the index is not created. + assert.deepStrictEqual(query({ QUERY_TEXT: 'anything' }).results, [], 'missing index returns no results'); + assert.ok(!(await client.sendCommand(['FT._LIST'])).includes('repo_memory_idx'), 'must not create the index'); + assert.strictEqual(await client.dbSize(), 0, 'missing-index query wrote nothing'); + + await client.ft.create('repo_memory_idx', { + type: { type: SchemaFieldTypes.TAG }, + repo: { type: SchemaFieldTypes.TAG }, + title: { type: SchemaFieldTypes.TEXT, WEIGHT: 2 }, + body_summary: { type: SchemaFieldTypes.TEXT }, + source_url: { type: SchemaFieldTypes.TEXT, NOSTEM: true }, + created_at: { type: SchemaFieldTypes.TEXT }, + tags: { type: SchemaFieldTypes.TAG, SEPARATOR: ',' }, + embedding: { + type: SchemaFieldTypes.VECTOR, + ALGORITHM: VectorAlgorithms.HNSW, + TYPE: 'FLOAT32', + DIM: 1536, + DISTANCE_METRIC: 'COSINE', + }, + }, { ON: 'HASH', PREFIX: 'memory:' }); + for (const [n, title] of ITEMS) { + await client.hSet(`memory:test_repo_pr_${n}`, { + id: `test_repo_pr_${n}`, type: 'pr_summary', repo: REPO, title, + body_summary: title, source_url: `https://github.com/${REPO}/pull/${n}`, + created_at: '2026-01-01T00:00:00Z', tags: '', + embedding: Buffer.from(new Float32Array(mockEmbedding(`${title}\n${title}\n`)).buffer), + }); + } + // An item from another repository must never be returned. + await client.hSet('memory:other_repo_pr_9', { + id: 'other_repo_pr_9', type: 'pr_summary', repo: 'other/repo', title: ITEMS[0][1], + body_summary: ITEMS[0][1], source_url: 'https://github.com/other/repo/pull/9', + created_at: '2026-01-01T00:00:00Z', tags: '', + embedding: Buffer.from(new Float32Array(mockEmbedding(ITEMS[0][1])).buffer), + }); + + // Twelve other-repository items closer to the query than any in-repo item must not crowd the + // in-repo matches out of the neighbors. + // The query adds words the in-repo item lacks, so the other-repository items match it better. + const nearQuery = 'Rewrite the sorted set tutorial to use ZRANGE with BYSCORE and REV for leaderboard scores and ranks'; + for (let i = 0; i < 12; i++) { + await client.hSet(`memory:other_repo_pr_${100 + i}`, { + id: `other_repo_pr_${100 + i}`, type: 'pr_summary', repo: 'other/repo', title: `${nearQuery} ${i}`, + body_summary: nearQuery, source_url: `https://github.com/other/repo/pull/${100 + i}`, + created_at: '2026-01-01T00:00:00Z', tags: '', + embedding: Buffer.from(new Float32Array(mockEmbedding(`${nearQuery} ${i}\n${nearQuery}\n`)).buffer), + }); + } + const crowded = query({ QUERY_TEXT: nearQuery }).results; + assert.ok(crowded.length >= 1 && /sorted set tutorial/.test(crowded[0].title), 'other repos do not crowd out in-repo matches'); + assert.ok(crowded.every(x => x.sourceUrl.includes(`/${REPO}/`)), 'only items from this repository'); + + // A limit above FT.SEARCH's default of 10 rows is honored. + for (let i = 0; i < 14; i++) { + const title = `Update client example number ${i} for the shared tutorial`; + await client.hSet(`memory:test_repo_pr_${200 + i}`, { + id: `test_repo_pr_${200 + i}`, type: 'pr_summary', repo: REPO, title, + body_summary: title, source_url: `https://github.com/${REPO}/pull/${200 + i}`, + created_at: '2026-01-01T00:00:00Z', tags: '', + embedding: Buffer.from(new Float32Array(mockEmbedding(`${title}\n${title}\n`)).buffer), + }); + } + assert.strictEqual(query({ QUERY_TEXT: 'Update client example for the shared tutorial', QUERY_MAX_DISTANCE: '1.9', QUERY_LIMIT: '13' }).results.length, 13, 'limit above 10'); + + const before = await client.dbSize(); + const q = 'Move the job-queue Jedis use case to RedisClient and close pipelines'; + + // Closest item first, other repositories excluded, full text absent without a token. + const r = query({ QUERY_TEXT: q, QUERY_FILES: 'content/develop/use-cases/job-queue/RedisJobQueue.java' }); + assert.ok(r.results.length >= 1, 'returns at least one result'); + assert.ok(/JedisPool to RedisClient/.test(r.results[0].title), `closest first, got: ${r.results[0].title}`); + assert.ok(r.results.every(x => x.sourceUrl.includes(`/${REPO}/`)), 'only items from this repository'); + assert.strictEqual(r.results[0].fullText, undefined, 'no fullText without a token'); + assert.deepStrictEqual(r.files, ['content/develop/use-cases/job-queue/RedisJobQueue.java']); + + // Limit and distance are honored. + assert.strictEqual(query({ QUERY_TEXT: q, QUERY_MAX_DISTANCE: '0.0001' }).results.length, 0, 'tight distance'); + const wide = query({ QUERY_TEXT: q, QUERY_MAX_DISTANCE: '1.5' }).results; + assert.ok(wide.length >= 3, 'wide distance returns several results'); + assert.deepStrictEqual(wide.map(x => x.score), wide.map(x => x.score).sort((x, y) => x - y), 'nearest first'); + assert.strictEqual(query({ QUERY_TEXT: q, QUERY_MAX_DISTANCE: '1.5', QUERY_LIMIT: '2' }).results.length, 2, 'limit'); + + // Read-only: nothing added, changed or indexed. + assert.strictEqual(await client.dbSize(), before, 'query wrote nothing'); + + // Missing inputs fail loudly. + assert.throws(() => query({ QUERY_TEXT: ' ' }), /QUERY_TEXT is empty/, 'empty query is an error'); + assert.throws(() => query({ QUERY_TEXT: q, REDIS_URL: '' }), /REDIS_URL is not set/, 'missing url is an error'); + + console.log('all repo-memory-query tests passed'); + } finally { + for (const k of await client.keys('memory:*')) await client.del(k); + await client.ft.dropIndex('repo_memory_idx').catch(() => {}); + await client.quit(); + } +} + +main().catch(err => { + console.error('FAILED:', err.message); + process.exit(1); +}); diff --git a/.github/workflows/repo-memory-query.yml b/.github/workflows/repo-memory-query.yml new file mode 100644 index 0000000000..e2d07c681b --- /dev/null +++ b/.github/workflows/repo-memory-query.yml @@ -0,0 +1,43 @@ +# Manually triggered, read-only search of the Repo Memory store from a description of planned +# work, before any PR exists. Run it with `gh workflow run repo-memory-query.yml -f query=...`. +# See .github/actions/repo-memory-query/README.md. Unlike repo-memory.yml it never writes to +# Redis and never comments on a PR. +name: Repo Memory Query + +on: + workflow_dispatch: + inputs: + query: + description: Describe the work you are about to do + required: true + type: string + files: + description: Files you plan to change (space-separated, optional) + required: false + type: string + limit: + description: Maximum results + required: false + default: '5' + type: string + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + query: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: .github/actions/repo-memory-query + + - uses: ./.github/actions/repo-memory-query + with: + redis-url: ${{ secrets.MEMORY_REDIS_URL }} + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + query: ${{ inputs.query }} + query-files: ${{ inputs.files }} + query-limit: ${{ inputs.limit }}