Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions backend/src/ai-core/tools/database-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ export function createDatabaseTools(isMongoDB: boolean): AIToolDefinition[] {
},
};

const searchDocumentationTool: AIToolDefinition = {
name: 'searchDocumentation',
description:
'Searches the official Rocketadmin documentation at https://docs.rocketadmin.com and returns the most relevant pages with their titles, URLs, and content snippets. Use this when the user asks how to use Rocketadmin features (connections, dashboards, permissions, groups, master password, widgets, integrations, settings, SSO, secrets, etc.) or when a question is about the product rather than the data in the connected database.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'A short search query describing what to look up in the Rocketadmin documentation.',
},
},
required: ['query'],
additionalProperties: false,
},
};

const tools: AIToolDefinition[] = [getTableStructureTool];

if (isMongoDB) {
Expand All @@ -61,6 +78,8 @@ export function createDatabaseTools(isMongoDB: boolean): AIToolDefinition[] {
tools.push(executeRawSqlTool);
}

tools.push(searchDocumentationTool);

return tools;
}

Expand Down
98 changes: 98 additions & 0 deletions backend/src/ai-core/tools/documentation-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import axios from 'axios';

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`;
Comment on lines +3 to +6
Comment on lines +3 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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 DEFAULT_HITS_PER_PAGE = 5;
const MAX_HITS_PER_PAGE = 10;
const MAX_CONTENT_LENGTH = 800;
const REQUEST_TIMEOUT_MS = 10000;

export interface DocumentationSearchHit {
title: string;
url: string;
content: string;
}

interface AlgoliaHierarchy {
lvl0?: string | null;
lvl1?: string | null;
lvl2?: string | null;
lvl3?: string | null;
lvl4?: string | null;
lvl5?: string | null;
lvl6?: string | null;
}

interface AlgoliaHit {
url?: string;
content?: string | null;
hierarchy?: AlgoliaHierarchy;
type?: string;
}

interface AlgoliaSearchResponse {
hits: AlgoliaHit[];
}

export async function searchDocumentation(query: string, hitsPerPage?: number): Promise<DocumentationSearchHit[]> {
const trimmedQuery = query?.trim();
if (!trimmedQuery) {
return [];
}

const limit = Math.min(Math.max(hitsPerPage ?? DEFAULT_HITS_PER_PAGE, 1), MAX_HITS_PER_PAGE);

const response = await axios.post<AlgoliaSearchResponse>(
ALGOLIA_SEARCH_URL,
{
query: trimmedQuery,
hitsPerPage: limit,
attributesToRetrieve: ['hierarchy', 'content', 'url', 'type'],
attributesToSnippet: ['content:50'],
},
Comment on lines +50 to +55
Comment on lines +53 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


🏁 Script executed:

# Find the file and examine its structure
find . -name "documentation-search.ts" -type f

Repository: rocket-admin/rocketadmin

Length of output: 120


🏁 Script executed:

# Check the file size first
wc -l backend/src/ai-core/tools/documentation-search.ts

Repository: 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.ts

Repository: 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 2

Repository: 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 -20

Repository: 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 test

Repository: 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.ts

Repository: 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.

{
headers: {
'X-Algolia-Application-Id': ALGOLIA_APP_ID,
'X-Algolia-API-Key': ALGOLIA_SEARCH_API_KEY,
'Content-Type': 'application/json',
},
timeout: REQUEST_TIMEOUT_MS,
},
);
Comment on lines +48 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.


const hits = response.data?.hits ?? [];
return hits.map(buildHit).filter((hit) => hit.url && (hit.title || hit.content));
}

function buildHit(hit: AlgoliaHit): DocumentationSearchHit {
const title = formatHierarchy(hit.hierarchy);
const rawContent = (hit.content ?? '').replace(/\s+/g, ' ').trim();
const content = rawContent.length > MAX_CONTENT_LENGTH ? `${rawContent.slice(0, MAX_CONTENT_LENGTH)}…` : rawContent;
return {
title,
url: hit.url ?? '',
content,
};
}

function formatHierarchy(hierarchy: AlgoliaHierarchy | undefined): string {
if (!hierarchy) {
return '';
}
const parts = [
hierarchy.lvl0,
hierarchy.lvl1,
hierarchy.lvl2,
hierarchy.lvl3,
hierarchy.lvl4,
hierarchy.lvl5,
hierarchy.lvl6,
]
.filter((part): part is string => Boolean(part))
.map((part) => part.replace(/​|‌|‍/g, '').trim())
.filter(Boolean);
return parts.join(' › ');
}
9 changes: 7 additions & 2 deletions backend/src/ai-core/tools/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Current date and time: ${currentDatetime}

Tool responses are encoded in TOON (Token-Oriented Object Notation) format - a compact, human-readable format similar to YAML with CSV-style tabular arrays. Parse it naturally.

Please follow these steps EXACTLY:
Please follow these steps EXACTLY when answering data questions:
1. First, always use the getTableStructure tool to analyze the table schema and understand available columns
2. If the question requires data from related tables, note their relationships
3. Generate an appropriate query that answers the user's question precisely
Expand All @@ -25,8 +25,13 @@ Please follow these steps EXACTLY:
6. After receiving query results, explain them to the user in a clear, conversational way
7. Include explanations of your approach when helpful

When the user asks how to use Rocketadmin itself (features, configuration, connections, dashboards, permissions, groups, master password, widgets, integrations, SSO, secrets, settings, API, etc.) rather than asking about the data in their database:
- Call the searchDocumentation tool with a concise query that captures the user's question
- Base your answer on the returned snippets and cite the relevant documentation URLs in your response
- You may combine searchDocumentation with the data tools when a question needs both product knowledge and data from the database

IMPORTANT:
- You MUST execute your generated queries using the appropriate tool - this is required for every question
- You MUST execute your generated queries using the appropriate tool - this is required for every data question
- After generating a SQL query, immediately call executeRawSql with that query
- For MongoDB databases, call executeAggregationPipeline with the aggregation pipeline
- The user cannot see the query results until you execute it with the appropriate tool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AIToolCall, AIToolDefinition } from '../../../ai-core/interfaces/ai-pro
import { AIProviderType } from '../../../ai-core/interfaces/ai-service.interface.js';
import { AICoreService } from '../../../ai-core/services/ai-core.service.js';
import { createDatabaseTools } from '../../../ai-core/tools/database-tools.js';
import { searchDocumentation } from '../../../ai-core/tools/documentation-search.js';
import { createDatabaseQuerySystemPrompt } from '../../../ai-core/tools/prompts.js';
import { isValidMongoDbCommand, isValidSQLQuery, wrapQueryWithLimit } from '../../../ai-core/tools/query-validators.js';
import { MessageBuilder } from '../../../ai-core/utils/message-builder.js';
Expand Down Expand Up @@ -275,6 +276,16 @@ export class RequestInfoFromTableWithAIUseCaseV7
break;
}

case 'searchDocumentation': {
const query = toolCall.arguments.query as string;
if (!query) {
throw new Error('Missing required function argument "query"');
}
Comment on lines +280 to +283
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

const docsResults = await searchDocumentation(query);
result = encodeToToon({ query, results: docsResults });
break;
}

default:
result = encodeError({ error: `Unknown tool: ${toolCall.name}` });
}
Expand Down
Loading
Loading