-
-
Notifications
You must be signed in to change notification settings - Fork 18
Backend aws bedrock #1482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backend aws bedrock #1482
Changes from all commits
e0be6d0
431f38e
83ba9b8
bf53577
83fa3b1
aa2ca39
3d27a6e
48ccf92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { ForeignKeyDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/foreign-key.ds.js'; | ||
| import { PrimaryKeyDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/primary-key.ds.js'; | ||
| import { TableStructureDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/table-structure.ds.js'; | ||
|
|
||
| export type TableInformation = { | ||
| table_name: string; | ||
| structure: Array<TableStructureDS>; | ||
| foreignKeys: Array<ForeignKeyDS>; | ||
| primaryColumns: Array<PrimaryKeyDS>; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,11 @@ | ||
| import { InTransactionEnum } from '../../enums/in-transaction.enum.js'; | ||
| import { FindOneConnectionDs } from '../connection/application/data-structures/find-one-connection.ds.js'; | ||
| import { RequestInfoFromTableDSV2 } from './application/data-structures/request-info-from-table.ds.js'; | ||
|
|
||
| export interface IRequestInfoFromTableV2 { | ||
| execute(inputData: RequestInfoFromTableDSV2, inTransaction: InTransactionEnum): Promise<void>; | ||
| } | ||
|
|
||
| export interface IAISettingsAndWidgetsCreation { | ||
| execute(connectionData: FindOneConnectionDs, inTransaction: InTransactionEnum): Promise<void>; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,218 @@ | ||||||||||
| import { Injectable } from '@nestjs/common'; | ||||||||||
| import { TableSettingsEntity } from '../table-settings/table-settings.entity.js'; | ||||||||||
| import { TableWidgetEntity } from '../widget/table-widget.entity.js'; | ||||||||||
| import { TableInformation } from './ai-data-entities/types/ai-module-types.js'; | ||||||||||
| import { AmazonBedrockAiProvider } from './amazon-bedrock/amazon-bedrock.ai.provider.js'; | ||||||||||
| import { QueryOrderingEnum } from '../../enums/query-ordering.enum.js'; | ||||||||||
| import { WidgetTypeEnum } from '../../enums/widget-type.enum.js'; | ||||||||||
| import { checkFieldAutoincrement } from '../../helpers/check-field-autoincrement.js'; | ||||||||||
|
|
||||||||||
| interface AIGeneratedTableSettings { | ||||||||||
| table_name: string; | ||||||||||
| display_name: string; | ||||||||||
| list_fields: string[]; | ||||||||||
| ordering_field: string | null; | ||||||||||
| ordering: 'ASC' | 'DESC'; | ||||||||||
| search_fields: string[]; | ||||||||||
| readonly_fields: string[]; | ||||||||||
| columns_view: string[]; | ||||||||||
| widgets: Array<{ | ||||||||||
| field_name: string; | ||||||||||
| widget_type: string; | ||||||||||
| name: string; | ||||||||||
| description: string; | ||||||||||
| }>; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| interface AIResponse { | ||||||||||
| tables: AIGeneratedTableSettings[]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| @Injectable() | ||||||||||
| export class AiService { | ||||||||||
| constructor(protected readonly aiProvider: AmazonBedrockAiProvider) {} | ||||||||||
|
|
||||||||||
| public async generateNewTableSettingsWithAI( | ||||||||||
| tablesInformation: Array<TableInformation>, | ||||||||||
| ): Promise<Array<TableSettingsEntity>> { | ||||||||||
| const prompt = this.buildPrompt(tablesInformation); | ||||||||||
| const aiResponse = await this.aiProvider.generateResponse(prompt); | ||||||||||
| const parsedResponse = this.parseAIResponse(aiResponse); | ||||||||||
| return this.buildTableSettingsEntities(parsedResponse, tablesInformation); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private buildPrompt(tablesInformation: Array<TableInformation>): string { | ||||||||||
| const widgetTypes = Object.values(WidgetTypeEnum).join(', '); | ||||||||||
|
|
||||||||||
| const tablesDescription = tablesInformation | ||||||||||
| .map((table) => { | ||||||||||
| const columns = table.structure | ||||||||||
| .map( | ||||||||||
| (col) => | ||||||||||
| ` - ${col.column_name}: ${col.data_type}${col.allow_null ? ' (nullable)' : ''}${checkFieldAutoincrement(col.column_default, col.extra) ? ' (auto_increment)' : ''}`, | ||||||||||
| ) | ||||||||||
| .join('\n'); | ||||||||||
| const primaryKeys = table.primaryColumns.map((pk) => pk.column_name).join(', '); | ||||||||||
| const foreignKeys = table.foreignKeys | ||||||||||
| .map((fk) => ` - ${fk.column_name} -> ${fk.referenced_table_name}.${fk.referenced_column_name}`) | ||||||||||
| .join('\n'); | ||||||||||
|
|
||||||||||
| return ` | ||||||||||
| Table: ${table.table_name} | ||||||||||
| Primary Keys: ${primaryKeys || 'none'} | ||||||||||
| Columns: | ||||||||||
| ${columns} | ||||||||||
| Foreign Keys: | ||||||||||
| ${foreignKeys || ' none'}`; | ||||||||||
| }) | ||||||||||
| .join('\n\n'); | ||||||||||
|
|
||||||||||
| 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. | ||||||||||
|
|
||||||||||
| For each table, provide: | ||||||||||
| 1. display_name: A human-readable name for the table | ||||||||||
| 2. list_fields: Columns to display in the table list view (most important columns first, max 5-7 columns) | ||||||||||
| 3. ordering_field: The best column to sort by default (usually created_at, id, or a timestamp) | ||||||||||
| 4. ordering: ASC or DESC | ||||||||||
| 5. search_fields: Columns that should be searchable | ||||||||||
| 6. readonly_fields: Columns that should not be editable (like auto_increment, timestamps) | ||||||||||
| 7. columns_view: All columns in preferred display order | ||||||||||
| 8. widgets: For each column, suggest the best widget type from: ${widgetTypes} | ||||||||||
|
|
||||||||||
| Available widget types and when to use them: | ||||||||||
| - Password: for password fields | ||||||||||
| - Boolean: for boolean/bit columns | ||||||||||
| - Date: for date columns | ||||||||||
| - Time: for time-only columns | ||||||||||
| - DateTime: for datetime/timestamp columns | ||||||||||
| - JSON: for JSON/JSONB columns | ||||||||||
| - Textarea: for long text fields (description, content, etc.) | ||||||||||
| - String: for short text fields (name, title, etc.) | ||||||||||
| - Readonly: for auto-generated fields | ||||||||||
| - Number: for numeric columns | ||||||||||
| - Select: for columns with limited options | ||||||||||
| - UUID: for UUID columns | ||||||||||
| - Enum: for enum columns | ||||||||||
| - Foreign_key: for foreign key columns | ||||||||||
| - File: for file path columns | ||||||||||
| - Image: for image URL columns | ||||||||||
| - URL: for URL columns | ||||||||||
| - Code: for code snippets | ||||||||||
| - Phone: for phone number columns | ||||||||||
| - Country: for country columns | ||||||||||
| - Color: for color columns (hex values) | ||||||||||
| - Range: for range values | ||||||||||
| - Timezone: for timezone columns | ||||||||||
|
|
||||||||||
| Database tables to analyze: | ||||||||||
| ${tablesDescription} | ||||||||||
|
|
||||||||||
| Respond ONLY with valid JSON in this exact format (no markdown, no explanations): | ||||||||||
| { | ||||||||||
| "tables": [ | ||||||||||
| { | ||||||||||
| "table_name": "table_name", | ||||||||||
| "display_name": "Human Readable Name", | ||||||||||
| "list_fields": ["col1", "col2"], | ||||||||||
| "ordering_field": "created_at", | ||||||||||
| "ordering": "DESC", | ||||||||||
| "search_fields": ["name", "email"], | ||||||||||
| "readonly_fields": ["id", "created_at"], | ||||||||||
| "columns_view": ["id", "name", "email", "created_at"], | ||||||||||
| "widgets": [ | ||||||||||
| { | ||||||||||
| "field_name": "column_name", | ||||||||||
| "widget_type": "String", | ||||||||||
| "name": "Column Display Name", | ||||||||||
| "description": "Description of what this column contains" | ||||||||||
| } | ||||||||||
| ] | ||||||||||
| } | ||||||||||
| ] | ||||||||||
| }`; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private parseAIResponse(aiResponse: string): AIResponse { | ||||||||||
| let cleanedResponse = aiResponse.trim(); | ||||||||||
| if (cleanedResponse.startsWith('```json')) { | ||||||||||
| cleanedResponse = cleanedResponse.slice(7); | ||||||||||
| } else if (cleanedResponse.startsWith('```')) { | ||||||||||
| cleanedResponse = cleanedResponse.slice(3); | ||||||||||
| } | ||||||||||
| if (cleanedResponse.endsWith('```')) { | ||||||||||
| cleanedResponse = cleanedResponse.slice(0, -3); | ||||||||||
| } | ||||||||||
| cleanedResponse = cleanedResponse.trim(); | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| return JSON.parse(cleanedResponse) as AIResponse; | ||||||||||
| } catch (error) { | ||||||||||
| throw new Error(`Failed to parse AI response: ${error.message}`); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private buildTableSettingsEntities( | ||||||||||
| aiResponse: AIResponse, | ||||||||||
| tablesInformation: Array<TableInformation>, | ||||||||||
| ): Array<TableSettingsEntity> { | ||||||||||
| return aiResponse.tables.map((tableSettings) => { | ||||||||||
| const tableInfo = tablesInformation.find((t) => t.table_name === tableSettings.table_name); | ||||||||||
| const validColumnNames = tableInfo?.structure.map((col) => col.column_name) || []; | ||||||||||
|
|
||||||||||
| const settings = new TableSettingsEntity(); | ||||||||||
| settings.table_name = tableSettings.table_name; | ||||||||||
| settings.display_name = tableSettings.display_name; | ||||||||||
| settings.list_fields = this.filterValidColumns(tableSettings.list_fields, validColumnNames); | ||||||||||
| settings.ordering_field = tableSettings.ordering_field; | ||||||||||
|
||||||||||
| settings.ordering_field = tableSettings.ordering_field; | |
| settings.ordering_field = validColumnNames.includes(tableSettings.ordering_field) | |
| ? tableSettings.ordering_field | |
| : null; |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mapWidgetType method returns WidgetTypeEnum | undefined, but when a widget type doesn't match any in the map, undefined is assigned to widget.widget_type. This could cause issues downstream if code expects widget_type to always have a valid enum value. Consider either filtering out widgets with unmapped types, using a default widget type (e.g., WidgetTypeEnum.String), or explicitly handling the undefined case in the validation logic.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export interface IAIProvider { | ||
| generateResponse(prompt: string): Promise<string>; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { Injectable } from '@nestjs/common'; | ||||||||||||||||||||||||||||||||||||||||
| import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; | ||||||||||||||||||||||||||||||||||||||||
| import { IAIProvider } from './ai-provider.interface.js'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||||||||||||||||||
| export class AmazonBedrockAiProvider implements IAIProvider { | ||||||||||||||||||||||||||||||||||||||||
| private readonly bedrockRuntimeClient: BedrockRuntimeClient; | ||||||||||||||||||||||||||||||||||||||||
| private readonly modelId: string = 'global.anthropic.claude-sonnet-4-5-20250929-v1:0'; | ||||||||||||||||||||||||||||||||||||||||
| private readonly temperature: number = 0.7; | ||||||||||||||||||||||||||||||||||||||||
| private readonly maxTokens: number = 1024; | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| private readonly region: string = 'us-west-2'; | ||||||||||||||||||||||||||||||||||||||||
| private readonly topP: number = 0.9; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| constructor() { | ||||||||||||||||||||||||||||||||||||||||
| this.bedrockRuntimeClient = new BedrockRuntimeClient({ | ||||||||||||||||||||||||||||||||||||||||
| region: this.region, | ||||||||||||||||||||||||||||||||||||||||
| credentials: { | ||||||||||||||||||||||||||||||||||||||||
| accessKeyId: process.env.AWS_ACCESS_KEY_ID, | ||||||||||||||||||||||||||||||||||||||||
| secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+19
|
||||||||||||||||||||||||||||||||||||||||
| this.bedrockRuntimeClient = new BedrockRuntimeClient({ | |
| region: this.region, | |
| credentials: { | |
| accessKeyId: process.env.AWS_ACCESS_KEY_ID, | |
| secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, | |
| const accessKeyId = process.env.AWS_ACCESS_KEY_ID; | |
| const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; | |
| if (!accessKeyId || !secretAccessKey) { | |
| throw new Error( | |
| 'AWS credentials are not configured. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.', | |
| ); | |
| } | |
| this.bedrockRuntimeClient = new BedrockRuntimeClient({ | |
| region: this.region, | |
| credentials: { | |
| accessKeyId, | |
| secretAccessKey, |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code accesses response.output.message?.content[0].text without checking if the content array has any elements. If the response has an empty content array, this will throw a runtime error. Add a check to ensure content array length is greater than 0 before accessing content[0].
| const responseText = response.output.message?.content[0].text; | |
| const content = response.output.message?.content; | |
| const responseText = content && content.length > 0 ? content[0]?.text : undefined; |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling throws a generic Error with a hardcoded message that loses the original error details. This makes debugging AI provider issues difficult. Consider including the original error message or using a more descriptive error that includes context about what failed (e.g., model ID, region, or specific AWS error codes).
| throw new Error('Failed to generate AI response.'); | |
| const originalMessage = | |
| error instanceof Error ? error.message : typeof error === 'string' ? error : JSON.stringify(error); | |
| throw new Error( | |
| `Failed to generate AI response (modelId=${this.modelId}, region=${this.region}): ${originalMessage}`, | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common'; | ||
| import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; | ||
| import { BaseType } from '../../../common/data-injection.tokens.js'; | ||
| import AbstractUseCase from '../../../common/abstract-use.case.js'; | ||
| import { IAISettingsAndWidgetsCreation } from '../ai-use-cases.interface.js'; | ||
| import { SharedJobsService } from '../../shared-jobs/shared-jobs.service.js'; | ||
| import { FindOneConnectionDs } from '../../connection/application/data-structures/find-one-connection.ds.js'; | ||
| import { Messages } from '../../../exceptions/text/messages.js'; | ||
|
|
||
| @Injectable({ scope: Scope.REQUEST }) | ||
| export class RequestAISettingsAndWidgetsCreationUseCase | ||
| extends AbstractUseCase<FindOneConnectionDs, void> | ||
| implements IAISettingsAndWidgetsCreation | ||
| { | ||
| constructor( | ||
| @Inject(BaseType.GLOBAL_DB_CONTEXT) | ||
| protected _dbContext: IGlobalDatabaseContext, | ||
| private readonly sharedJobsService: SharedJobsService, | ||
| ) { | ||
| super(); | ||
| } | ||
|
|
||
| public async implementation(connectionData: FindOneConnectionDs): Promise<void> { | ||
| const { connectionId, masterPwd } = connectionData; | ||
|
|
||
| const connection = await this._dbContext.connectionRepository.findAndDecryptConnection(connectionId, masterPwd); | ||
| if (!connection) { | ||
| throw new BadRequestException(Messages.CONNECTION_NOT_FOUND); | ||
| } | ||
|
|
||
| await this.sharedJobsService.scanDatabaseAndCreateSettingsAndWidgetsWithAI(connection); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error parsing logic in this method does not account for potential issues when accessing error.message. If the error caught is not an Error object (e.g., a string or other type is thrown), accessing error.message will result in undefined. Consider using a type guard or checking the error type before accessing its message property.