Skip to content

Commit d9a5f93

Browse files
authored
Merge pull request #1482 from rocket-admin/backend_aws_bedrock
Backend aws bedrock
2 parents 9fdd9ef + 48ccf92 commit d9a5f93

24 files changed

+1036
-27
lines changed

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@amplitude/node": "1.10.2",
29+
"@aws-sdk/client-bedrock-runtime": "^3.954.0",
2930
"@aws-sdk/lib-dynamodb": "^3.953.0",
3031
"@electric-sql/pglite": "^0.3.14",
3132
"@faker-js/faker": "^10.1.0",

backend/src/app.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import { SignInAuditModule } from './entities/user-sign-in-audit/sign-in-audit.m
8484
TableCategoriesModule,
8585
UserSecretModule,
8686
SignInAuditModule,
87+
AIModule,
8788
],
8889
controllers: [AppController],
8990
providers: [

backend/src/common/data-injection.tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export enum UseCaseType {
154154
DELETE_API_KEY = 'DELETE_API_KEY',
155155

156156
REQUEST_INFO_FROM_TABLE_WITH_AI_V2 = 'REQUEST_INFO_FROM_TABLE_WITH_AI_V2',
157+
REQUEST_AI_SETTINGS_AND_WIDGETS_CREATION = 'REQUEST_AI_SETTINGS_AND_WIDGETS_CREATION',
157158

158159
CREATE_TABLE_FILTERS = 'CREATE_TABLE_FILTERS',
159160
FIND_TABLE_FILTERS = 'FIND_TABLE_FILTERS',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ForeignKeyDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/foreign-key.ds.js';
2+
import { PrimaryKeyDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/primary-key.ds.js';
3+
import { TableStructureDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/table-structure.ds.js';
4+
5+
export type TableInformation = {
6+
table_name: string;
7+
structure: Array<TableStructureDS>;
8+
foreignKeys: Array<ForeignKeyDS>;
9+
primaryColumns: Array<PrimaryKeyDS>;
10+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
2+
import { FindOneConnectionDs } from '../connection/application/data-structures/find-one-connection.ds.js';
23
import { RequestInfoFromTableDSV2 } from './application/data-structures/request-info-from-table.ds.js';
34

45
export interface IRequestInfoFromTableV2 {
56
execute(inputData: RequestInfoFromTableDSV2, inTransaction: InTransactionEnum): Promise<void>;
67
}
8+
9+
export interface IAISettingsAndWidgetsCreation {
10+
execute(connectionData: FindOneConnectionDs, inTransaction: InTransactionEnum): Promise<void>;
11+
}

backend/src/entities/ai/ai.module.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
1+
import { Global, MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { AuthMiddleware } from '../../authorization/auth.middleware.js';
44
import { GlobalDatabaseContext } from '../../common/application/global-database-context.js';
55
import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js';
66
import { LogOutEntity } from '../log-out/log-out.entity.js';
77
import { UserEntity } from '../user/user.entity.js';
8+
import { AiService } from './ai.service.js';
9+
import { AmazonBedrockAiProvider } from './amazon-bedrock/amazon-bedrock.ai.provider.js';
810
import { RequestInfoFromTableWithAIUseCaseV4 } from './use-cases/request-info-from-table-with-ai-v4.use.case.js';
911
import { UserAIRequestsControllerV2 } from './user-ai-requests-v2.controller.js';
12+
import { RequestAISettingsAndWidgetsCreationUseCase } from './use-cases/request-ai-settings-and-widgets-creation.use.case.js';
1013

14+
@Global()
1115
@Module({
1216
imports: [TypeOrmModule.forFeature([UserEntity, LogOutEntity])],
1317
providers: [
@@ -19,11 +23,23 @@ import { UserAIRequestsControllerV2 } from './user-ai-requests-v2.controller.js'
1923
provide: UseCaseType.REQUEST_INFO_FROM_TABLE_WITH_AI_V2,
2024
useClass: RequestInfoFromTableWithAIUseCaseV4,
2125
},
26+
{
27+
provide: UseCaseType.REQUEST_AI_SETTINGS_AND_WIDGETS_CREATION,
28+
useClass: RequestAISettingsAndWidgetsCreationUseCase,
29+
},
30+
AmazonBedrockAiProvider,
31+
AiService,
2232
],
33+
exports: [AiService, AmazonBedrockAiProvider],
2334
controllers: [UserAIRequestsControllerV2],
2435
})
2536
export class AIModule implements NestModule {
2637
public configure(consumer: MiddlewareConsumer): any {
27-
consumer.apply(AuthMiddleware).forRoutes({ path: '/ai/v2/request/:connectionId', method: RequestMethod.POST });
38+
consumer
39+
.apply(AuthMiddleware)
40+
.forRoutes(
41+
{ path: '/ai/v2/request/:connectionId', method: RequestMethod.POST },
42+
{ path: '/ai/v2/setup/:connectionId', method: RequestMethod.GET },
43+
);
2844
}
2945
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { TableSettingsEntity } from '../table-settings/table-settings.entity.js';
3+
import { TableWidgetEntity } from '../widget/table-widget.entity.js';
4+
import { TableInformation } from './ai-data-entities/types/ai-module-types.js';
5+
import { AmazonBedrockAiProvider } from './amazon-bedrock/amazon-bedrock.ai.provider.js';
6+
import { QueryOrderingEnum } from '../../enums/query-ordering.enum.js';
7+
import { WidgetTypeEnum } from '../../enums/widget-type.enum.js';
8+
import { checkFieldAutoincrement } from '../../helpers/check-field-autoincrement.js';
9+
10+
interface AIGeneratedTableSettings {
11+
table_name: string;
12+
display_name: string;
13+
list_fields: string[];
14+
ordering_field: string | null;
15+
ordering: 'ASC' | 'DESC';
16+
search_fields: string[];
17+
readonly_fields: string[];
18+
columns_view: string[];
19+
widgets: Array<{
20+
field_name: string;
21+
widget_type: string;
22+
name: string;
23+
description: string;
24+
}>;
25+
}
26+
27+
interface AIResponse {
28+
tables: AIGeneratedTableSettings[];
29+
}
30+
31+
@Injectable()
32+
export class AiService {
33+
constructor(protected readonly aiProvider: AmazonBedrockAiProvider) {}
34+
35+
public async generateNewTableSettingsWithAI(
36+
tablesInformation: Array<TableInformation>,
37+
): Promise<Array<TableSettingsEntity>> {
38+
const prompt = this.buildPrompt(tablesInformation);
39+
const aiResponse = await this.aiProvider.generateResponse(prompt);
40+
const parsedResponse = this.parseAIResponse(aiResponse);
41+
return this.buildTableSettingsEntities(parsedResponse, tablesInformation);
42+
}
43+
44+
private buildPrompt(tablesInformation: Array<TableInformation>): string {
45+
const widgetTypes = Object.values(WidgetTypeEnum).join(', ');
46+
47+
const tablesDescription = tablesInformation
48+
.map((table) => {
49+
const columns = table.structure
50+
.map(
51+
(col) =>
52+
` - ${col.column_name}: ${col.data_type}${col.allow_null ? ' (nullable)' : ''}${checkFieldAutoincrement(col.column_default, col.extra) ? ' (auto_increment)' : ''}`,
53+
)
54+
.join('\n');
55+
const primaryKeys = table.primaryColumns.map((pk) => pk.column_name).join(', ');
56+
const foreignKeys = table.foreignKeys
57+
.map((fk) => ` - ${fk.column_name} -> ${fk.referenced_table_name}.${fk.referenced_column_name}`)
58+
.join('\n');
59+
60+
return `
61+
Table: ${table.table_name}
62+
Primary Keys: ${primaryKeys || 'none'}
63+
Columns:
64+
${columns}
65+
Foreign Keys:
66+
${foreignKeys || ' none'}`;
67+
})
68+
.join('\n\n');
69+
70+
return `You are a database administration assistant. Analyze the following database tables and generate optimal settings for displaying and managing them in a web admin panel.
71+
72+
For each table, provide:
73+
1. display_name: A human-readable name for the table
74+
2. list_fields: Columns to display in the table list view (most important columns first, max 5-7 columns)
75+
3. ordering_field: The best column to sort by default (usually created_at, id, or a timestamp)
76+
4. ordering: ASC or DESC
77+
5. search_fields: Columns that should be searchable
78+
6. readonly_fields: Columns that should not be editable (like auto_increment, timestamps)
79+
7. columns_view: All columns in preferred display order
80+
8. widgets: For each column, suggest the best widget type from: ${widgetTypes}
81+
82+
Available widget types and when to use them:
83+
- Password: for password fields
84+
- Boolean: for boolean/bit columns
85+
- Date: for date columns
86+
- Time: for time-only columns
87+
- DateTime: for datetime/timestamp columns
88+
- JSON: for JSON/JSONB columns
89+
- Textarea: for long text fields (description, content, etc.)
90+
- String: for short text fields (name, title, etc.)
91+
- Readonly: for auto-generated fields
92+
- Number: for numeric columns
93+
- Select: for columns with limited options
94+
- UUID: for UUID columns
95+
- Enum: for enum columns
96+
- Foreign_key: for foreign key columns
97+
- File: for file path columns
98+
- Image: for image URL columns
99+
- URL: for URL columns
100+
- Code: for code snippets
101+
- Phone: for phone number columns
102+
- Country: for country columns
103+
- Color: for color columns (hex values)
104+
- Range: for range values
105+
- Timezone: for timezone columns
106+
107+
Database tables to analyze:
108+
${tablesDescription}
109+
110+
Respond ONLY with valid JSON in this exact format (no markdown, no explanations):
111+
{
112+
"tables": [
113+
{
114+
"table_name": "table_name",
115+
"display_name": "Human Readable Name",
116+
"list_fields": ["col1", "col2"],
117+
"ordering_field": "created_at",
118+
"ordering": "DESC",
119+
"search_fields": ["name", "email"],
120+
"readonly_fields": ["id", "created_at"],
121+
"columns_view": ["id", "name", "email", "created_at"],
122+
"widgets": [
123+
{
124+
"field_name": "column_name",
125+
"widget_type": "String",
126+
"name": "Column Display Name",
127+
"description": "Description of what this column contains"
128+
}
129+
]
130+
}
131+
]
132+
}`;
133+
}
134+
135+
private parseAIResponse(aiResponse: string): AIResponse {
136+
let cleanedResponse = aiResponse.trim();
137+
if (cleanedResponse.startsWith('```json')) {
138+
cleanedResponse = cleanedResponse.slice(7);
139+
} else if (cleanedResponse.startsWith('```')) {
140+
cleanedResponse = cleanedResponse.slice(3);
141+
}
142+
if (cleanedResponse.endsWith('```')) {
143+
cleanedResponse = cleanedResponse.slice(0, -3);
144+
}
145+
cleanedResponse = cleanedResponse.trim();
146+
147+
try {
148+
return JSON.parse(cleanedResponse) as AIResponse;
149+
} catch (error) {
150+
throw new Error(`Failed to parse AI response: ${error.message}`);
151+
}
152+
}
153+
154+
private buildTableSettingsEntities(
155+
aiResponse: AIResponse,
156+
tablesInformation: Array<TableInformation>,
157+
): Array<TableSettingsEntity> {
158+
return aiResponse.tables.map((tableSettings) => {
159+
const tableInfo = tablesInformation.find((t) => t.table_name === tableSettings.table_name);
160+
const validColumnNames = tableInfo?.structure.map((col) => col.column_name) || [];
161+
162+
const settings = new TableSettingsEntity();
163+
settings.table_name = tableSettings.table_name;
164+
settings.display_name = tableSettings.display_name;
165+
settings.list_fields = this.filterValidColumns(tableSettings.list_fields, validColumnNames);
166+
settings.ordering_field = tableSettings.ordering_field;
167+
settings.ordering = tableSettings.ordering === 'DESC' ? QueryOrderingEnum.DESC : QueryOrderingEnum.ASC;
168+
settings.search_fields = this.filterValidColumns(tableSettings.search_fields, validColumnNames);
169+
settings.readonly_fields = this.filterValidColumns(tableSettings.readonly_fields, validColumnNames);
170+
settings.columns_view = this.filterValidColumns(tableSettings.columns_view, validColumnNames);
171+
settings.table_widgets = tableSettings.widgets
172+
.filter((w) => validColumnNames.includes(w.field_name))
173+
.map((widgetData) => {
174+
const widget = new TableWidgetEntity();
175+
widget.field_name = widgetData.field_name;
176+
widget.widget_type = this.mapWidgetType(widgetData.widget_type);
177+
widget.name = widgetData.name;
178+
widget.description = widgetData.description;
179+
return widget;
180+
});
181+
182+
return settings;
183+
});
184+
}
185+
186+
private filterValidColumns(columns: string[], validColumnNames: string[]): string[] {
187+
return columns?.filter((col) => validColumnNames.includes(col)) || [];
188+
}
189+
190+
private mapWidgetType(widgetType: string): WidgetTypeEnum | undefined {
191+
const widgetTypeMap = new Map<string, WidgetTypeEnum>([
192+
['Password', WidgetTypeEnum.Password],
193+
['Boolean', WidgetTypeEnum.Boolean],
194+
['Date', WidgetTypeEnum.Date],
195+
['Time', WidgetTypeEnum.Time],
196+
['DateTime', WidgetTypeEnum.DateTime],
197+
['JSON', WidgetTypeEnum.JSON],
198+
['Textarea', WidgetTypeEnum.Textarea],
199+
['String', WidgetTypeEnum.String],
200+
['Readonly', WidgetTypeEnum.Readonly],
201+
['Number', WidgetTypeEnum.Number],
202+
['Select', WidgetTypeEnum.Select],
203+
['UUID', WidgetTypeEnum.UUID],
204+
['Enum', WidgetTypeEnum.Enum],
205+
['Foreign_key', WidgetTypeEnum.Foreign_key],
206+
['File', WidgetTypeEnum.File],
207+
['Image', WidgetTypeEnum.Image],
208+
['URL', WidgetTypeEnum.URL],
209+
['Code', WidgetTypeEnum.Code],
210+
['Phone', WidgetTypeEnum.Phone],
211+
['Country', WidgetTypeEnum.Country],
212+
['Color', WidgetTypeEnum.Color],
213+
['Range', WidgetTypeEnum.Range],
214+
['Timezone', WidgetTypeEnum.Timezone],
215+
]);
216+
return widgetTypeMap.get(widgetType);
217+
}
218+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface IAIProvider {
2+
generateResponse(prompt: string): Promise<string>;
3+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';
3+
import { IAIProvider } from './ai-provider.interface.js';
4+
5+
@Injectable()
6+
export class AmazonBedrockAiProvider implements IAIProvider {
7+
private readonly bedrockRuntimeClient: BedrockRuntimeClient;
8+
private readonly modelId: string = 'global.anthropic.claude-sonnet-4-5-20250929-v1:0';
9+
private readonly temperature: number = 0.7;
10+
private readonly maxTokens: number = 1024;
11+
private readonly region: string = 'us-west-2';
12+
private readonly topP: number = 0.9;
13+
14+
constructor() {
15+
this.bedrockRuntimeClient = new BedrockRuntimeClient({
16+
region: this.region,
17+
credentials: {
18+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
19+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
20+
},
21+
});
22+
}
23+
public async generateResponse(prompt: string): Promise<string> {
24+
const conversation = [
25+
{
26+
role: 'user' as const,
27+
content: [{ text: prompt }],
28+
},
29+
];
30+
31+
const command = new ConverseCommand({
32+
modelId: this.modelId,
33+
messages: conversation,
34+
inferenceConfig: { maxTokens: this.maxTokens, temperature: this.temperature, topP: this.topP },
35+
});
36+
try {
37+
const response = await this.bedrockRuntimeClient.send(command);
38+
const responseText = response.output.message?.content[0].text;
39+
return responseText || 'No response generated.';
40+
} catch (error) {
41+
console.error('Error generating AI response:', error);
42+
throw new Error('Failed to generate AI response.');
43+
}
44+
}
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common';
2+
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
3+
import { BaseType } from '../../../common/data-injection.tokens.js';
4+
import AbstractUseCase from '../../../common/abstract-use.case.js';
5+
import { IAISettingsAndWidgetsCreation } from '../ai-use-cases.interface.js';
6+
import { SharedJobsService } from '../../shared-jobs/shared-jobs.service.js';
7+
import { FindOneConnectionDs } from '../../connection/application/data-structures/find-one-connection.ds.js';
8+
import { Messages } from '../../../exceptions/text/messages.js';
9+
10+
@Injectable({ scope: Scope.REQUEST })
11+
export class RequestAISettingsAndWidgetsCreationUseCase
12+
extends AbstractUseCase<FindOneConnectionDs, void>
13+
implements IAISettingsAndWidgetsCreation
14+
{
15+
constructor(
16+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
17+
protected _dbContext: IGlobalDatabaseContext,
18+
private readonly sharedJobsService: SharedJobsService,
19+
) {
20+
super();
21+
}
22+
23+
public async implementation(connectionData: FindOneConnectionDs): Promise<void> {
24+
const { connectionId, masterPwd } = connectionData;
25+
26+
const connection = await this._dbContext.connectionRepository.findAndDecryptConnection(connectionId, masterPwd);
27+
if (!connection) {
28+
throw new BadRequestException(Messages.CONNECTION_NOT_FOUND);
29+
}
30+
31+
await this.sharedJobsService.scanDatabaseAndCreateSettingsAndWidgetsWithAI(connection);
32+
}
33+
}

0 commit comments

Comments
 (0)