feat: add searchDocumentation tool for querying Rocketadmin documentation and update prompts for AI responses#1787
Conversation
…tion and update prompts for AI responses
📝 WalkthroughWalkthroughThis PR adds Algolia-backed documentation search as a new AI tool. The assistant can now query Rocketadmin documentation, retrieve relevant pages with URLs and content snippets, and cite documentation in responses to product usage questions. ChangesDocumentation Search Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new AI tool (searchDocumentation) that queries Rocketadmin’s public documentation (via Algolia) so the AI can answer product/how-to questions using official docs, and wires that tool into the existing AI request flow and system prompt guidance.
Changes:
- Added
searchDocumentationtool implementation (Algolia HTTP client + result shaping). - Registered the new tool in the AI tools list and implemented tool-call handling in the AI request use case.
- Updated the system prompt to instruct the AI when to use documentation search, and added unit + e2e tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/ai-core/tools/documentation-search.ts | Implements Algolia-backed documentation search + hit formatting/truncation |
| backend/src/ai-core/tools/database-tools.ts | Adds searchDocumentation to the tool definitions exposed to the model |
| backend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.ts | Executes searchDocumentation tool calls and encodes results into TOON |
| backend/src/ai-core/tools/prompts.ts | Updates system prompt to route product questions to searchDocumentation |
| backend/test/ava-tests/non-saas-tests/non-saas-documentation-search.test.ts | Unit tests for searchDocumentation behavior (blank query, parsing, truncation, clamp, filtering) |
| backend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.ts | E2E test ensuring tool_call triggers Algolia request and AI message persistence |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const ALGOLIA_APP_ID = '31P3X3M1EE'; | ||
| const ALGOLIA_SEARCH_API_KEY = 'fe7422b190b4ec77f8e60c80a3a3ed8a'; | ||
| const ALGOLIA_INDEX_NAME = 'rocketadmin-docs'; | ||
| const ALGOLIA_SEARCH_URL = `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/${ALGOLIA_INDEX_NAME}/query`; |
| { | ||
| query: trimmedQuery, | ||
| hitsPerPage: limit, | ||
| attributesToRetrieve: ['hierarchy', 'content', 'url', 'type'], | ||
| attributesToSnippet: ['content:50'], | ||
| }, |
| hierarchy.lvl6, | ||
| ] | ||
| .filter((part): part is string => Boolean(part)) | ||
| .map((part) => part.replace(/||/g, '').trim()) |
| nock(ALGOLIA_ORIGIN) | ||
| .post(ALGOLIA_PATH, (body) => body.query === 'master password' && body.hitsPerPage === 5) | ||
| .matchHeader('x-algolia-application-id', '31P3X3M1EE') | ||
| .matchHeader('x-algolia-api-key', 'fe7422b190b4ec77f8e60c80a3a3ed8a') |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
backend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.ts (1)
44-52: ⚡ Quick winUse a typed arrow helper for
buildIterableStream.This helper is a function declaration without an explicit return type. Convert it to a typed
constarrow to match repository TS rules.Suggested refactor
-function buildIterableStream(chunks: Array<Record<string, unknown>>) { - return { - async *[Symbol.asyncIterator]() { - for (const chunk of chunks) { - yield chunk; - } - }, - }; -} +const buildIterableStream = ( + chunks: Array<Record<string, unknown>>, +): AsyncIterable<Record<string, unknown>> => ({ + async *[Symbol.asyncIterator](): AsyncGenerator<Record<string, unknown>> { + for (const chunk of chunks) { + yield chunk; + } + }, +});Per coding guidelines for
**/*.ts: "Prefer arrow functions over function declarations" and "Always add type annotations to function parameters and return types in TypeScript".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.ts` around lines 44 - 52, The function declaration buildIterableStream should be converted to a typed const arrow helper with explicit parameter and return types to follow repository TS rules; replace the current function buildIterableStream(chunks: Array<Record<string, unknown>>) { ... } with a const buildIterableStream: (chunks: Array<Record<string, unknown>>) => AsyncIterable<Record<string, unknown>> = (chunks) => { async *[Symbol.asyncIterator]() { ... } } (ensuring the async generator yields Record<string, unknown> and the overall return type is AsyncIterable<Record<string, unknown>>>).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/src/ai-core/tools/documentation-search.ts`:
- Around line 3-6: Replace the hard-coded Algolia credential and related
constants by reading the API key (and optionally APP_ID and INDEX_NAME) from
runtime config/environment: stop using the ALGOLIA_SEARCH_API_KEY constant and
instead read process.env.ALGOLIA_SEARCH_API_KEY (or your app's config accessor)
and throw a clear error if missing; update ALGOLIA_APP_ID and ALGOLIA_INDEX_NAME
to be loaded from env vars (or keep defaults) and rebuild ALGOLIA_SEARCH_URL
using those runtime values; ensure tests set the env vars or inject fixture
values so CI uses a test key and no secret stays in source control.
- Around line 48-64: The axios.post call that assigns to response (calling
ALGOLIA_SEARCH_URL with REQUEST_TIMEOUT_MS) is not wrapped in error handling;
wrap that call in a try/catch, detect AxiosError (use axios.isAxiosError) and
translate network/timeouts/5xx into the tool's transient-failure mode (e.g.,
throw or return a specific ToolError/DocumentSearchError with a clear message
including status, response data or timeout info) instead of letting the raw
Axios exception bubble up; ensure you log the error context (query, limit,
ALGOLIA_APP_ID/URL) and preserve original error details in the new error for
debugging.
- Around line 53-55: The buildHit function is returning raw hit.content instead
of Algolia's snippet payload; update buildHit to prefer
hit._snippetResult?.content?.value when present and fall back to hit.content,
add the _snippetResult field to the AlgoliaHit interface, and adjust tests that
reference Algolia hits (the ones covering buildHit) to mock the real Algolia
response shape including _snippetResult.content.value so assertions validate the
snippet usage instead of full content.
In
`@backend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.ts`:
- Around line 280-283: The current check lets whitespace-only queries pass;
update the validation around the `query` extracted from `toolCall.arguments` so
you first ensure it's a string and then trim it before testing length (e.g.,
verify typeof query === 'string' and query.trim().length > 0); if the trimmed
value is empty throw the same Error('Missing required function argument
"query"') and use the trimmed value for the subsequent documentation search to
avoid calling external docs with blank input.
In
`@backend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.ts`:
- Around line 131-135: In test.beforeEach (the block that resets
iterationCounter and capturedSearchQuery), make HTTP mocking deterministic by
disabling real outbound network calls after nock.cleanAll(); call
nock.disableNetConnect() (or nock.enableNetConnect(false)) so any unmatched
requests will be blocked/fail the test; if the test requires local network
(e.g., localhost), add an allow rule with nock.enableNetConnect('127.0.0.1') or
similar before tests run. This change should be made adjacent to the existing
test.beforeEach that references iterationCounter and capturedSearchQuery.
---
Nitpick comments:
In
`@backend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.ts`:
- Around line 44-52: The function declaration buildIterableStream should be
converted to a typed const arrow helper with explicit parameter and return types
to follow repository TS rules; replace the current function
buildIterableStream(chunks: Array<Record<string, unknown>>) { ... } with a const
buildIterableStream: (chunks: Array<Record<string, unknown>>) =>
AsyncIterable<Record<string, unknown>> = (chunks) => { async
*[Symbol.asyncIterator]() { ... } } (ensuring the async generator yields
Record<string, unknown> and the overall return type is
AsyncIterable<Record<string, unknown>>>).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3c2fe02c-a193-473c-8617-083eb672c18c
📒 Files selected for processing (6)
backend/src/ai-core/tools/database-tools.tsbackend/src/ai-core/tools/documentation-search.tsbackend/src/ai-core/tools/prompts.tsbackend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.tsbackend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.tsbackend/test/ava-tests/non-saas-tests/non-saas-documentation-search.test.ts
| const ALGOLIA_APP_ID = '31P3X3M1EE'; | ||
| const ALGOLIA_SEARCH_API_KEY = 'fe7422b190b4ec77f8e60c80a3a3ed8a'; | ||
| const ALGOLIA_INDEX_NAME = 'rocketadmin-docs'; | ||
| const ALGOLIA_SEARCH_URL = `https://${ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/${ALGOLIA_INDEX_NAME}/query`; |
There was a problem hiding this comment.
Move the Algolia API key out of source control.
Keeping a live key in the module makes rotation harder and leaves credential material in git history permanently. Load it from runtime config/env instead and have tests inject a fixture value.
🧰 Tools
🪛 Betterleaks (1.2.0)
[high] 4-4: Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms.
(algolia-api-key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/ai-core/tools/documentation-search.ts` around lines 3 - 6,
Replace the hard-coded Algolia credential and related constants by reading the
API key (and optionally APP_ID and INDEX_NAME) from runtime config/environment:
stop using the ALGOLIA_SEARCH_API_KEY constant and instead read
process.env.ALGOLIA_SEARCH_API_KEY (or your app's config accessor) and throw a
clear error if missing; update ALGOLIA_APP_ID and ALGOLIA_INDEX_NAME to be
loaded from env vars (or keep defaults) and rebuild ALGOLIA_SEARCH_URL using
those runtime values; ensure tests set the env vars or inject fixture values so
CI uses a test key and no secret stays in source control.
| const response = await axios.post<AlgoliaSearchResponse>( | ||
| ALGOLIA_SEARCH_URL, | ||
| { | ||
| query: trimmedQuery, | ||
| hitsPerPage: limit, | ||
| attributesToRetrieve: ['hierarchy', 'content', 'url', 'type'], | ||
| attributesToSnippet: ['content:50'], | ||
| }, | ||
| { | ||
| headers: { | ||
| 'X-Algolia-Application-Id': ALGOLIA_APP_ID, | ||
| 'X-Algolia-API-Key': ALGOLIA_SEARCH_API_KEY, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| timeout: REQUEST_TIMEOUT_MS, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Handle Algolia request failures explicitly.
A timeout or 5xx from Algolia will currently bubble a raw Axios exception through the AI flow. Wrap this call in try/catch and translate it to the tool’s intended failure mode so a transient search outage does not take down the whole response path. As per coding guidelines, "Ensure all error handling is explicit - use try/catch blocks appropriately"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/ai-core/tools/documentation-search.ts` around lines 48 - 64, The
axios.post call that assigns to response (calling ALGOLIA_SEARCH_URL with
REQUEST_TIMEOUT_MS) is not wrapped in error handling; wrap that call in a
try/catch, detect AxiosError (use axios.isAxiosError) and translate
network/timeouts/5xx into the tool's transient-failure mode (e.g., throw or
return a specific ToolError/DocumentSearchError with a clear message including
status, response data or timeout info) instead of letting the raw Axios
exception bubble up; ensure you log the error context (query, limit,
ALGOLIA_APP_ID/URL) and preserve original error details in the new error for
debugging.
| attributesToRetrieve: ['hierarchy', 'content', 'url', 'type'], | ||
| attributesToSnippet: ['content:50'], | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Algolia Search API responses, when a request includes attributesToSnippet: ['content:50'], is the query-matched snippet returned under _snippetResult.content.valuewhile the originalcontent attribute remains unchanged?
💡 Result:
Yes, that is correct. When you include attributesToSnippet: ['content:50'] in an Algolia search request, the original content attribute in the hit remains unchanged [1][2]. The snippeted version of that attribute is returned separately within the _snippetResult object in the search response, specifically under _snippetResult.content.value [1][2]. This _snippetResult object contains the shortened text with highlighting tags applied to the matched query terms [1][3]. The original attribute is preserved in the main hit object, allowing you to access both the full content and the snippeted, highlighted version as needed [4][5].
Citations:
- 1: https://algolia.com/doc/api-reference/api-parameters/attributesToSnippet
- 2: https://samypesse.gitbook.io/algolia-faq/searching/what-is-attributes-to-snippet-how-does-it-work
- 3: https://algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js
- 4: https://www.algolia.com/doc/api-reference/widgets/snippet/react
- 5: https://algolia.com/doc/api-reference/widgets/snippet/js
🏁 Script executed:
# Find the file and examine its structure
find . -name "documentation-search.ts" -type fRepository: rocket-admin/rocketadmin
Length of output: 120
🏁 Script executed:
# Check the file size first
wc -l backend/src/ai-core/tools/documentation-search.tsRepository: rocket-admin/rocketadmin
Length of output: 121
🏁 Script executed:
# Read the file to see the implementation
cat -n backend/src/ai-core/tools/documentation-search.tsRepository: rocket-admin/rocketadmin
Length of output: 3256
🏁 Script executed:
# Also search for buildHit function to understand how it processes the content
rg "buildHit|_snippetResult" backend/src/ai-core/tools/ -A 5 -B 2Repository: rocket-admin/rocketadmin
Length of output: 1060
🏁 Script executed:
# Find test files for documentation-search
find . -name "*documentation-search*test*" -o -name "*documentation-search*spec*" | head -20Repository: rocket-admin/rocketadmin
Length of output: 146
🏁 Script executed:
# Search for test files that might import this module
rg "documentation-search" --type ts --type tsx -l | grep -i testRepository: rocket-admin/rocketadmin
Length of output: 96
🏁 Script executed:
# Look for any test files in the ai-core/tools directory
find backend/src/ai-core/tools -name "*.test.ts" -o -name "*.spec.ts"Repository: rocket-admin/rocketadmin
Length of output: 50
🏁 Script executed:
# Read the test file
cat -n ./backend/test/ava-tests/non-saas-tests/non-saas-documentation-search.test.tsRepository: rocket-admin/rocketadmin
Length of output: 4395
Use Algolia's snippet payload instead of raw content.
attributesToSnippet: ['content:50'] requests query-matched snippets from Algolia, which are returned in _snippetResult.content.value. However, buildHit currently uses hit.content, returning the full content (up to 800 chars) instead of the contextual snippet. Update the code to use _snippetResult.content.value when available, and add the _snippetResult field to the AlgoliaHit interface. Also update the tests (lines 29–84) to mock the actual Algolia response shape including _snippetResult.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/ai-core/tools/documentation-search.ts` around lines 53 - 55, The
buildHit function is returning raw hit.content instead of Algolia's snippet
payload; update buildHit to prefer hit._snippetResult?.content?.value when
present and fall back to hit.content, add the _snippetResult field to the
AlgoliaHit interface, and adjust tests that reference Algolia hits (the ones
covering buildHit) to mock the real Algolia response shape including
_snippetResult.content.value so assertions validate the snippet usage instead of
full content.
| const query = toolCall.arguments.query as string; | ||
| if (!query) { | ||
| throw new Error('Missing required function argument "query"'); | ||
| } |
There was a problem hiding this comment.
Reject whitespace-only query values before calling documentation search.
if (!query) allows " " through and still triggers the external docs lookup. Trim and type-check before validation.
Suggested fix
- const query = toolCall.arguments.query as string;
- if (!query) {
+ const query =
+ typeof toolCall.arguments?.query === 'string'
+ ? toolCall.arguments.query.trim()
+ : '';
+ if (!query) {
throw new Error('Missing required function argument "query"');
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@backend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.ts`
around lines 280 - 283, The current check lets whitespace-only queries pass;
update the validation around the `query` extracted from `toolCall.arguments` so
you first ensure it's a string and then trim it before testing length (e.g.,
verify typeof query === 'string' and query.trim().length > 0); if the trimmed
value is empty throw the same Error('Missing required function argument
"query"') and use the trimmed value for the subsequent documentation search to
avoid calling external docs with blank input.
| test.beforeEach(() => { | ||
| iterationCounter = 0; | ||
| capturedSearchQuery = null; | ||
| nock.cleanAll(); | ||
| }); |
There was a problem hiding this comment.
Make HTTP mocking deterministic by blocking unmatched outbound network calls.
nock.cleanAll() resets interceptors but still allows unmatched real requests. That can hide accidental Algolia calls in the "empty query" test.
Suggested fix
test.beforeEach(() => {
iterationCounter = 0;
capturedSearchQuery = null;
nock.cleanAll();
+ nock.disableNetConnect();
+ nock.enableNetConnect(/(127\.0\.0\.1|localhost)/);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test.beforeEach(() => { | |
| iterationCounter = 0; | |
| capturedSearchQuery = null; | |
| nock.cleanAll(); | |
| }); | |
| test.beforeEach(() => { | |
| iterationCounter = 0; | |
| capturedSearchQuery = null; | |
| nock.cleanAll(); | |
| nock.disableNetConnect(); | |
| nock.enableNetConnect(/(127\.0\.0\.1|localhost)/); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@backend/test/ava-tests/non-saas-tests/non-saas-ai-search-documentation-tool-e2e.test.ts`
around lines 131 - 135, In test.beforeEach (the block that resets
iterationCounter and capturedSearchQuery), make HTTP mocking deterministic by
disabling real outbound network calls after nock.cleanAll(); call
nock.disableNetConnect() (or nock.enableNetConnect(false)) so any unmatched
requests will be blocked/fail the test; if the test requires local network
(e.g., localhost), add an allow rule with nock.enableNetConnect('127.0.0.1') or
similar before tests run. This change should be made adjacent to the existing
test.beforeEach that references iterationCounter and capturedSearchQuery.
Summary by CodeRabbit
New Features
Tests