Compare commits

...
Author SHA1 Message Date
ruanwebdev 8d04c1d495 FEAT: add logic to native documentation with AI 2025-09-23 16:28:38 -03:00
8 changed files with 1024 additions and 51 deletions
Binary file not shown.
+137
View File
@@ -42,6 +42,33 @@
]
}
},
"/auth/sign-out": {
"post": {
"operationId": "AuthController_signOut",
"parameters": [
{
"name": "dadosfera-lang",
"in": "header",
"required": false,
"schema": {
"enum": [
"pt-br",
"en-us"
],
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"tags": [
"Auth"
]
}
},
"/auth/refresh-access-token": {
"post": {
"operationId": "AuthController_refreshAccessToken",
@@ -5135,6 +5162,116 @@
]
}
},
"/catalog/data-asset/{nimbus_id}/docs/ai": {
"post": {
"operationId": "CatalogController_saveDocumentation",
"summary": "Save documentation for data asset",
"description": "Saves documentation content for a data asset",
"parameters": [
{
"name": "dadosfera-lang",
"in": "header",
"required": false,
"schema": {
"enum": [
"pt-br",
"en-us"
],
"type": "string"
}
},
{
"name": "nimbus_id",
"required": true,
"in": "path",
"description": "Nimbus ID of the data asset",
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "Documentation saved successfully"
}
},
"tags": [
"Catalog"
],
"security": [
{
"access-token": []
}
]
}
},
"/catalog/data-asset/{nimbus_id}/docs/generate-ai": {
"post": {
"operationId": "CatalogController_generateAiDocumentation",
"summary": "Generate AI documentation for data asset",
"description": "Generates comprehensive documentation for a data asset using AI (Autodrive)",
"parameters": [
{
"name": "dadosfera-lang",
"in": "header",
"required": false,
"schema": {
"enum": [
"pt-br",
"en-us"
],
"type": "string"
}
},
{
"name": "nimbus_id",
"required": true,
"in": "path",
"description": "Nimbus ID of the data asset",
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "AI documentation generated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"documentation": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "Bad request - invalid nimbus_id or missing data"
},
"404": {
"description": "Data asset not found"
},
"500": {
"description": "Internal server error during AI generation"
}
},
"tags": [
"Catalog"
],
"security": [
{
"access-token": []
}
]
}
},
"/customers/{id}/mfa": {
"post": {
"operationId": "CustomersController_enableMfaEnforce",
+8
View File
@@ -16,6 +16,14 @@ declare global {
OPEN_CUSTOMER_ID: string;
DEDICATED_PROXY: string;
COOKIE_SECRET: string;
// Autodrive Configuration
AUTODRIVE_USERNAME?: string;
AUTODRIVE_PASSWORD?: string;
AUTODRIVE_BASE_URL?: string;
AUTODRIVE_MODEL?: string;
AUTODRIVE_KEY?: string;
AUTO_DRIVE_KEY?: string;
}
}
}
+109 -50
View File
@@ -22,6 +22,9 @@ import {
ApiHeaders,
ApiOkResponse,
ApiTags,
ApiOperation,
ApiParam,
ApiResponse,
} from '@nestjs/swagger';
import {
Authenticated,
@@ -88,10 +91,6 @@ export class CatalogController {
@Query() query: ICatalogAllRequest,
): Promise<ICatalogAllResponse> {
const { user_id, customer_name, customer_id, username, permissions } = user;
this.logger.info(`/catalog - searchCatalog`, {
user_id,
customer_name,
});
const is_data_manager = permissions.includes(
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
@@ -128,10 +127,6 @@ export class CatalogController {
@Res() res: Response
) {
const { user_id, customer_name, customer_id, username, permissions } = user;
this.logger.info(`/catalog/download - searchCatalog`, {
user_id,
customer_name,
});
const is_data_manager = permissions.includes(
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
@@ -168,10 +163,6 @@ export class CatalogController {
async findByPipelineAndObject(@User() user: RequestUser, @Query() query) {
const { username, user_id, customer_id, customer_name, permissions } = user;
const { pipeline, object } = query;
this.logger.info(`/catalog - ON GET DATA ASSET BY PIPELINE AND OBJECT`, {
username,
customer_name,
});
if (!pipeline || !object) {
throw new BadRequestException('Query params not provided');
@@ -224,10 +215,6 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
async findAllTags(@Body() body) {
this.logger.info(`/catalog - ON FIND ALL TAGS ROUTE`, {
user: body.info.user_id,
customer: body.info.customer,
});
const { user_id, customer, customer_id } = body.info;
const metadata = PackTheMetadata({
@@ -253,10 +240,6 @@ export class CatalogController {
) {
const { username, user_id, customer_id, customer_name, permissions } = user;
this.logger.info(`GET /data-asset/${id}`, {
username,
customer_name,
});
const is_data_manager = permissions.includes(
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
@@ -309,10 +292,6 @@ export class CatalogController {
) {
const { username, user_id, customer_id, customer_name, permissions } = user;
this.logger.info(`/catalog - ON GET ONE DASHBOARD METABASE ROUTE`, {
username,
customer_name,
});
const is_data_manager = permissions.includes(
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
@@ -364,10 +343,6 @@ export class CatalogController {
): Promise<IColumnsMetadataResponse> {
const { customer_name, customer_id, user_id, username } = user;
this.logger.info(`/catalog - columns-metadata`, {
user_id,
customer_name,
});
const metadata = PackTheMetadata({
customer_name,
@@ -395,10 +370,6 @@ export class CatalogController {
): Promise<IPreviewResponse> {
const { customer_name, customer_id, user_id, username, customer_modules } = user;
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
user_id,
customer_name,
});
const metadata = PackTheMetadata({
customer_name,
@@ -424,24 +395,31 @@ export class CatalogController {
@Language() language: LanguageEnum,
@Param('id') id: string,
): Promise<IDocsResponse> {
const { customer_name, customer_id, user_id, username } = user;
try {
const { customer_name, customer_id, user_id, username } = user;
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
user_id,
customer_name,
});
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
user_id,
customer_name,
id,
});
const metadata = PackTheMetadata({
customer_name,
customer_id,
user_id,
username,
language,
});
const metadata = PackTheMetadata({
customer_name,
customer_id,
user_id,
username,
language,
});
const docs = await this.catalogService.getDataDocs(id, metadata);
const docs = await this.catalogService.getDataDocs(id, metadata);
return { docs };
return { docs };
} catch (error) {
this.logger.error(`Error in getDataAssetDocs for id ${id}: ${error.message}`);
this.logger.error(`Error details: ${JSON.stringify(error)}`);
throw error;
}
}
@Put('data-asset/:id')
@@ -490,10 +468,6 @@ export class CatalogController {
) {
const { user_id, customer_name } = user;
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
user_id,
customer_name,
});
const res = await this.catalogService.createDataDocs({
table_id,
@@ -506,6 +480,7 @@ export class CatalogController {
return res;
}
@ApiInternalOnlyEndpoint()
@Put('data-asset/:id/manage-permissions')
async manageDataAssetPermissions(
@@ -931,4 +906,88 @@ export class CatalogController {
this.logger.error(error.message);
}
}
@Post('/data-asset/:nimbus_id/docs/ai')
@ApiOperation({
summary: 'Save documentation for data asset',
description: 'Saves documentation content for a data asset',
})
@ApiParam({
name: 'nimbus_id',
description: 'Nimbus ID of the data asset',
type: 'string',
})
@ApiResponse({
status: 201,
description: 'Documentation saved successfully',
})
async saveDocumentation(
@Param('nimbus_id') nimbusId: string,
@Body() body: { docs: string },
@User() user: RequestUser,
) {
const metadata = PackTheMetadata(user);
try {
await this.catalogService.updateDataAssetDocumentation(nimbusId, body.docs, metadata);
return {
message: 'Documentation saved successfully',
};
} catch (error) {
this.logger.error(`Error saving documentation for ${nimbusId}: ${error.message}`);
throw error;
}
}
@Post('/data-asset/:nimbus_id/docs/generate-ai')
@ApiOperation({
summary: 'Generate AI documentation for data asset',
description: 'Generates comprehensive documentation for a data asset using AI (Autodrive)',
})
@ApiParam({
name: 'nimbus_id',
description: 'Nimbus ID of the data asset',
type: 'string',
})
@ApiResponse({
status: 201,
description: 'AI documentation generated successfully',
schema: {
type: 'object',
properties: {
message: { type: 'string' },
documentation: { type: 'string' },
},
},
})
@ApiResponse({
status: 400,
description: 'Bad request - invalid nimbus_id or missing data',
})
@ApiResponse({
status: 404,
description: 'Data asset not found',
})
@ApiResponse({
status: 500,
description: 'Internal server error during AI generation',
})
async generateAiDocumentation(
@Param('nimbus_id') dataAssetId: string,
@User() user: RequestUser,
) {
const metadata = PackTheMetadata(user);
try {
const result = await this.catalogService.generateAiDocumentation(dataAssetId, metadata, user);
return {
message: 'AI documentation generated successfully',
documentation: result,
};
} catch (error) {
this.logger.error(`Error generating AI documentation for ${dataAssetId}: ${error.message}`);
throw error;
}
}
}
+580 -1
View File
@@ -1,4 +1,5 @@
import DadosferaLogger from '@dadosfera/dadosfera-logs/dist';
import FormData from 'form-data';
import {
WriteService,
ReadService,
@@ -38,6 +39,20 @@ import {
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
import { TypeParser } from 'src/utils/FileParser/parser-types';
import { ParserBuilder } from 'src/utils/FileParser/parser.builder';
import { AI_DOCUMENTATION_PROMPT, AI_DOCUMENTATION_CONFIG } from './prompts/ai-documentation.prompt';
import { AUTODRIVE_CONSTANTS, GEOGRAPHIC_KEYS, COMMON_COUNTRIES } from './constants/autodrive.constants';
import {
AutodriveCredentials,
AutodriveAskPayload,
AutodriveAskResponse,
AutodriveAnswerResponse,
AutodriveUploadResponse,
DatasetStatusResponse,
ColumnData,
ColumnsMetadata,
DataPreview,
FormattedDataForAI
} from './types/ai-documentation.types';
class CatalogService implements OnModuleInit {
catalogReadService: ReadService.CatalogReadServices;
@@ -336,15 +351,44 @@ class CatalogService implements OnModuleInit {
}
async getDataDocs(id: string, metadata: Metadata) {
const { documentation } = await lastValueFrom(
let documentation: string;
try {
const response = await lastValueFrom(
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
);
const doc = response.documentation;
documentation = doc;
if (!documentation) {
this.logger.warn(`No documentation found for id: ${id}`);
return null;
}
// Verificar se a documentação é um JSON válido
if (documentation.trim().startsWith('{') || documentation.trim().startsWith('[')) {
const docs = JSON.parse(documentation);
return docs;
} else {
this.logger.warn(`Documentation is not JSON format, returning as raw text for id: ${id}`);
return { raw_documentation: documentation };
}
} catch (error) {
this.logger.error(`getDataDocs failed for id ${id}: ${error.message}`);
// Se for erro de JSON parsing, tentar retornar a documentação como string
if (error.message.includes('JSON') || error.message.includes('parse')) {
this.logger.warn(`JSON parsing failed, returning raw documentation for id: ${id}`);
return { raw_documentation: documentation || 'No documentation available' };
}
throw error;
}
}
async getDatasetPreview(id: string, metadata: Metadata) {
try {
const { preview } = await lastValueFrom(
this.catalogReadService.GetDatasetPreview(
{ id, type: undefined },
@@ -353,19 +397,35 @@ class CatalogService implements OnModuleInit {
);
const result = JSON.parse(preview);
return result;
} catch (error) {
this.logger.error(`getDatasetPreview failed for id ${id}: ${error.message}`);
throw error;
}
}
async getDatasetColumnsMetadata(id: string, metadata: Metadata) {
try {
const { columns_metadata } = await lastValueFrom(
this.catalogReadService.GetDatasetColumnsMetadata(
{ id, type: undefined },
metadata,
),
);
if (!columns_metadata || columns_metadata.length <= 2) {
this.logger.warn(`Empty or minimal response from gRPC: "${columns_metadata}"`);
throw new Error('Empty response from gRPC service');
}
const result = JSON.parse(columns_metadata);
return result;
} catch (error) {
this.logger.error(`getDatasetColumnsMetadata failed for id ${id}: ${error.message}`);
throw error;
}
}
async createDataDocs(body) {
const nimbusUrl = this._getNimbusUrl(body);
const { data } = await axios.post(
@@ -375,6 +435,7 @@ class CatalogService implements OnModuleInit {
return data;
}
async findAllTags(data, metadata) {
this.logger.info('CatalogService - findAllCustomerTags');
@@ -605,6 +666,524 @@ class CatalogService implements OnModuleInit {
);
return res;
}
private capitalizeFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
async updateDataAssetDocumentation(nimbusId: string, documentation: string, metadata: Metadata): Promise<void> {
try {
if (!documentation) {
this.logger.warn(`Documentation is undefined for ${nimbusId}`);
return;
}
const changes = {
documentation: documentation
};
await lastValueFrom(
this.catalogWriteService.UpdateDataAsset(
{ id: nimbusId, changes: JSON.stringify(changes) },
metadata,
),
);
this.logger.info(`Documentation updated successfully for ${nimbusId}`);
} catch (error) {
this.logger.error(`Error updating documentation for ${nimbusId}: ${error.message}`);
throw error;
}
}
async generateAiDocumentation(dataAssetId: string, metadata: Metadata, user?: any): Promise<string> {
this.logger.info(`Generating AI documentation for data asset: ${dataAssetId}`);
try {
const customer_id = metadata.get('customer_id')?.[0]?.toString();
// 1. Buscar metadados do data asset
const dataAsset = await this.getOneDataAsset({
id: dataAssetId,
customer_id,
metadata
});
if (!dataAsset || !dataAsset.data_asset || !dataAsset.data_asset.nimbus_id) {
throw new Error("Nimbus ID not found in data asset metadata");
}
const nimbusId = dataAsset.data_asset.nimbus_id.toString();
// 2. Buscar informações das colunas
let columnsData = null;
try {
columnsData = await this.getDatasetColumnsMetadata(dataAssetId, metadata);
} catch (error) {
this.logger.warn(`Failed to get column metadata: ${error.message}`);
columnsData = null;
}
// 2.1. Fallback para dados das colunas
const dataAssetWithColumns = dataAsset as any;
if (!columnsData && dataAssetWithColumns.columns && Array.isArray(dataAssetWithColumns.columns)) {
columnsData = { columns: dataAssetWithColumns.columns };
} else if (!columnsData) {
const numColumns = parseInt(dataAsset.data_asset.num_columns) || 0;
if (numColumns > 0) {
columnsData = {
columns: Array.from({ length: numColumns }, (_, i) => ({
name: `COLUMN_${i + 1}`,
type: 'UNKNOWN',
description: 'Column information not available',
nullable: 'UNKNOWN'
}))
};
}
}
// 3. Buscar preview dos dados (opcional)
let dataPreview = [];
try {
dataPreview = await this.getDatasetPreview(nimbusId, metadata);
} catch (error) {
this.logger.warn(`Failed to get data preview: ${error.message}`);
dataPreview = [];
}
// 4. Formatar dados e chamar Autodrive
const combinedText = this.formatDataForAI(dataAsset, dataPreview, columnsData);
const documentation = await this.callAutodriveForDocumentation(combinedText, user?.access_token);
// 5. Salvar a documentação
const createDataDocsPayload = {
table_id: dataAsset.data_asset.nimbus_id,
docs: documentation,
info: {
customer: user.customer_name,
},
};
await this.createDataDocs(createDataDocsPayload);
this.logger.info(`AI documentation generated and saved successfully for ${dataAssetId}, length: ${documentation.length} chars`);
return documentation;
} catch (error) {
this.logger.error(`Error generating AI documentation for ${dataAssetId}: ${error.message}`);
let errorMessage = error.message;
if (error.response) {
errorMessage = `API Error ${error.response.status}: ${error.response.data?.detail || error.message}`;
}
throw new HttpException(
`Failed to generate AI documentation: ${errorMessage}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private formatDataForAI(dataAsset: any, dataPreview: any[], columnsData?: any): string {
let combinedText = '';
// Seguir exatamente o padrão do Python: chamar json_to_free_text para cada objeto separadamente
// 1. metadata_data (dataAsset)
combinedText += this.jsonToFreeText(dataAsset);
// 2. info_data (columns) - usar columnsData se disponível, senão tentar dataAsset.columns
if (columnsData) {
// Adicionar aviso se os dados das colunas são limitados
if (columnsData.columns && columnsData.columns.some((col: any) => col.name?.startsWith('COLUMN_'))) {
combinedText += 'WARNING: Column information is limited or unavailable. Generated column names are placeholders.\n\n';
}
combinedText += this.jsonToFreeText(columnsData);
} else if (dataAsset.columns && Array.isArray(dataAsset.columns)) {
const columnsDataFromAsset = { columns: dataAsset.columns };
combinedText += this.jsonToFreeText(columnsDataFromAsset);
} else {
// Se não há dados das colunas, informar explicitamente
combinedText += 'WARNING: No column metadata available. Cannot generate detailed table structure.\n\n';
}
// 3. preview_data
if (dataPreview && dataPreview.length > 0) {
const previewData = { preview: dataPreview };
combinedText += this.jsonToFreeText(previewData);
}
return combinedText;
}
private jsonToFreeText(jsonObj: any, indentLevel: number = 0): string {
if (!jsonObj || typeof jsonObj !== 'object') {
return '';
}
let text = '';
const indent = ' '.repeat(indentLevel);
// Processa primeiro os dados geográficos se existirem (igual ao Python)
const geoKeys = GEOGRAPHIC_KEYS;
// Adiciona uma seção especial para dados de preview se existirem (igual ao Python)
if ('preview' in jsonObj) {
text += "=== PREVIEW DATA START ===\n";
if (Array.isArray(jsonObj['preview'])) {
// Primeiro, vamos procurar por colunas geográficas
const geoColumns = [];
if (jsonObj['preview'].length > 0 && typeof jsonObj['preview'][0] === 'object') {
for (const key of Object.keys(jsonObj['preview'][0])) {
const keyLower = key.toLowerCase();
if (geoKeys.some(geoTerm => keyLower.includes(geoTerm))) {
geoColumns.push(key);
}
}
}
// Se encontramos colunas geográficas, vamos destacá-las
if (geoColumns.length > 0) {
text += "GEOGRAPHIC DATA FOUND IN COLUMNS:\n";
for (const col of geoColumns) {
text += `=== Column: ${col} ===\n`;
const values = jsonObj['preview'].map(row => String(row[col] || '')).filter(v => v);
text += "Values: " + values.join(", ") + "\n\n";
}
}
// Agora processamos todos os dados normalmente
for (const row of jsonObj['preview']) {
text += this.jsonToFreeText(row, indentLevel + 1);
}
} else {
text += this.jsonToFreeText(jsonObj['preview'], indentLevel + 1);
}
text += "=== PREVIEW DATA END ===\n\n";
}
// Processa o resto dos dados (igual ao Python)
for (const [key, value] of Object.entries(jsonObj)) {
if (key === 'preview') { // Skip preview here since we handled it above
continue;
}
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
text += `${indent}${this.capitalize(key)}:\n`;
for (const item of value) {
if (typeof item === 'object' && item !== null) {
text += this.jsonToFreeText(item, indentLevel + 1);
} else {
text += `${indent} - ${item}\n`;
}
}
} else {
text += `${indent}${this.capitalize(key)}:\n`;
text += this.jsonToFreeText(value, indentLevel + 1);
}
} else {
// Destaca campos geográficos
if (geoKeys.includes(key.toLowerCase() as any)) {
text += `${indent}!!! GEOGRAPHIC DATA !!! ${this.capitalize(key)}: ${value}\n`;
} else {
text += `${indent}${this.capitalize(key)}: ${value}\n`;
}
}
}
text += '\n';
return text;
}
private capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
private async callAutodriveForDocumentation(combinedText: string, userAccessToken?: string): Promise<string> {
const credentials = this.getAutodriveCredentials();
try {
// Criar dataset temporário com os dados reais
const datasetId = await this.createTemporaryDataset(combinedText, userAccessToken);
// Fazer pergunta ao dataset
const documentation = await this.askQuestionToDataset(datasetId, credentials);
return documentation;
} catch (error) {
this.logger.error(`Error calling Autodrive API: ${error.message}`);
throw new Error(`Autodrive API call failed: ${error.message}`);
}
}
private getAutodriveCredentials(): AutodriveCredentials {
const fallbackCredentials = process.env.AUTODRIVE_KEY || process.env.AUTO_DRIVE_KEY;
// Verificar se as credenciais principais estão disponíveis
if (AUTODRIVE_CONSTANTS.USERNAME && AUTODRIVE_CONSTANTS.PASSWORD) {
return {
username: AUTODRIVE_CONSTANTS.USERNAME,
password: AUTODRIVE_CONSTANTS.PASSWORD,
baseUrl: AUTODRIVE_CONSTANTS.BASE_URL,
model: AUTODRIVE_CONSTANTS.DEFAULT_MODEL,
};
}
// Fallback para credenciais alternativas
if (fallbackCredentials) {
const cleanKey = fallbackCredentials.startsWith("'") && fallbackCredentials.endsWith("'")
? fallbackCredentials.slice(1, -1)
: fallbackCredentials;
return {
username: '',
password: '',
baseUrl: AUTODRIVE_CONSTANTS.BASE_URL,
model: AUTODRIVE_CONSTANTS.DEFAULT_MODEL,
authHeader: `Basic ${cleanKey}`,
};
}
// Fallback de emergência removido por segurança
// Configure as variáveis de ambiente necessárias
throw new Error('No authentication credentials available. Please configure AUTODRIVE_USERNAME and AUTODRIVE_PASSWORD in your .env file');
}
private async askQuestionToDataset(datasetId: string, credentials: AutodriveCredentials): Promise<string> {
const authHeader = credentials.authHeader || this.createAuthHeader(credentials);
const askPayload: AutodriveAskPayload = {
question: AI_DOCUMENTATION_PROMPT.trim(),
fetch_k: AI_DOCUMENTATION_CONFIG.FETCH_K,
k: AI_DOCUMENTATION_CONFIG.K,
model: credentials.model
};
const askHeaders = {
'Authorization': authHeader,
...AUTODRIVE_CONSTANTS.HEADERS
};
const askResponse = await axios.post<AutodriveAskResponse>(
`${credentials.baseUrl}/dataset/${datasetId}/ai_question`,
askPayload,
{
headers: askHeaders,
timeout: AUTODRIVE_CONSTANTS.ASK_TIMEOUT
}
);
// Verificar se a resposta contém a documentação diretamente
if (askResponse.data?.answer) {
const documentation = askResponse.data.answer;
this.logger.info(`Documentation generated successfully - Length: ${documentation.length} characters`);
return documentation;
}
// Se não contém a resposta diretamente, verificar se tem question_id para buscar
if (askResponse.data?.question_id) {
const questionId = askResponse.data.question_id;
// Buscar a resposta da pergunta
const answerResponse = await axios.get<AutodriveAnswerResponse>(
`${credentials.baseUrl}/dataset/${datasetId}/ai_question/${questionId}`,
{
headers: { 'Authorization': authHeader },
timeout: AUTODRIVE_CONSTANTS.ANSWER_TIMEOUT
}
);
// Se a resposta ainda está sendo processada, fazer polling contínuo
if (answerResponse.data?.status === 'started' || !answerResponse.data?.answer) {
let attempts = 0;
const maxAttempts = 12;
const pollInterval = 10000;
while (attempts < maxAttempts) {
attempts++;
await new Promise(resolve => setTimeout(resolve, pollInterval));
try {
const pollResponse = await axios.get<AutodriveAnswerResponse>(
`${credentials.baseUrl}/dataset/${datasetId}/ai_question/${questionId}`,
{
headers: { 'Authorization': authHeader },
timeout: AUTODRIVE_CONSTANTS.ANSWER_TIMEOUT
}
);
// Se a resposta está pronta, retornar
if (pollResponse.data?.answer && pollResponse.data?.status !== 'started') {
const documentation = pollResponse.data.answer;
this.logger.info(`Documentation generated successfully after ${attempts} attempts - Length: ${documentation.length} characters`);
return documentation;
}
// Se ainda está processando, continuar o loop
if (pollResponse.data?.status === 'started') {
continue;
}
// Se falhou, mostrar erro e quebrar
if (pollResponse.data?.status === 'failed') {
throw new Error(`Answer processing failed: ${pollResponse.data.status_reason || 'Unknown error'}`);
}
// Se houve erro ou status inesperado, quebrar o loop
break;
} catch (pollError) {
this.logger.error(`Polling attempt ${attempts} failed: ${pollError.message}`);
continue;
}
}
throw new Error(`Answer still not ready after ${maxAttempts} polling attempts`);
}
if (!answerResponse.data || !answerResponse.data.answer) {
throw new Error(`Answer response invalid: ${JSON.stringify(answerResponse.data)}`);
}
const documentation = answerResponse.data.answer;
this.logger.info(`Documentation generated successfully - Length: ${documentation.length} characters`);
return documentation;
}
throw new Error(`Autodrive API response invalid: ${JSON.stringify(askResponse.data)}`);
}
private async createTemporaryDataset(combinedText: string, userAccessToken?: string): Promise<string> {
if (!AUTODRIVE_CONSTANTS.BASE_URL) {
throw new Error('AUTODRIVE_BASE_URL not configured');
}
if (!combinedText || combinedText.trim().length === 0) {
throw new Error('combinedText is empty - cannot create dataset');
}
const credentials = this.getAutodriveCredentials();
const authHeader = credentials.authHeader || this.createAuthHeader(credentials);
const tempFilePath = await this.createTempFile(combinedText);
try {
const uploadResponse = await this.uploadDataset(tempFilePath, authHeader);
const datasetId = uploadResponse.dataset_id;
await this.waitForDatasetReady(datasetId, authHeader, credentials.baseUrl);
return datasetId;
} catch (error) {
this.logger.error(`Error creating temporary dataset: ${error.message}`);
throw error;
} finally {
await this.cleanupTempFile(tempFilePath);
}
}
private async createTempFile(combinedText: string): Promise<string> {
const tempFileName = `temp_data_${Date.now()}.txt`;
const tempFilePath = `/tmp/${tempFileName}`;
const fs = await import('fs');
fs.writeFileSync(tempFilePath, combinedText, 'utf8');
return tempFilePath;
}
private async uploadDataset(tempFilePath: string, authHeader: string): Promise<AutodriveUploadResponse> {
const tempFileName = tempFilePath.split('/').pop() || 'temp_data.txt';
const fs = await import('fs');
const formData = new FormData();
formData.append('files', fs.createReadStream(tempFilePath), tempFileName);
formData.append('name', tempFileName);
const uploadResponse = await axios.post<AutodriveUploadResponse>(
`${AUTODRIVE_CONSTANTS.BASE_URL}/upload`,
formData,
{
headers: {
'Authorization': authHeader,
...formData.getHeaders(),
},
timeout: 30000, // 30 segundos
}
);
if (!uploadResponse.data?.dataset_id) {
throw new Error('No dataset_id in upload response');
}
return uploadResponse.data;
}
private async waitForDatasetReady(datasetId: string, authHeader: string, autodriveBaseUrl: string): Promise<void> {
const maxAttempts = 12;
const pollInterval = 10000; // 10 segundos
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const statusResponse = await axios.get<DatasetStatusResponse>(
`${autodriveBaseUrl}/dataset/${datasetId}`,
{
headers: { 'Authorization': authHeader },
timeout: 30000,
}
);
if (statusResponse.data?.status === 'success') {
return;
}
if (statusResponse.data?.status === 'failed') {
throw new Error(`Dataset processing failed: ${statusResponse.data.status_reason || 'Unknown error'}`);
}
if (statusResponse.data?.status === 'processing') {
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
continue;
}
}
if (attempt === maxAttempts) {
throw new Error(`Dataset still not ready after ${maxAttempts} attempts`);
}
} catch (statusError) {
this.logger.error(`Status check error on attempt ${attempt}: ${statusError.message}`);
if (attempt === maxAttempts) {
throw statusError;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
}
private async cleanupTempFile(tempFilePath: string | null): Promise<void> {
if (!tempFilePath) return;
try {
const fs = await import('fs');
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath);
}
} catch (error) {
this.logger.warn(`Failed to remove temporary file: ${error.message}`);
}
}
private createAuthHeader(credentials: AutodriveCredentials): string {
return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`;
}
}
export { CatalogService };
@@ -0,0 +1,42 @@
/**
* Constantes relacionadas ao Autodrive
*
*/
export const AUTODRIVE_CONSTANTS = {
// URLs e endpoints (apenas do ENV)
BASE_URL: process.env.BASE_URL_AUTODRIVE || process.env.AUTODRIVE_BASE_URL,
// Credenciais (apenas do ENV, sem fallback para segurança)
USERNAME: process.env.AUTODRIVE_USERNAME,
PASSWORD: process.env.AUTODRIVE_PASSWORD,
// Modelo padrão
DEFAULT_MODEL: process.env.AUTODRIVE_MODEL || "gpt-4o",
// Timeouts (em milissegundos)
ASK_TIMEOUT: 120000,
ANSWER_TIMEOUT: 180000,
// Headers
HEADERS: {
'Content-Type': 'application/json',
},
} as const;
/**
* Chaves geográficas para detecção de dados de localização
*/
export const GEOGRAPHIC_KEYS = [
'country', 'countries', 'city', 'cities',
'region', 'regions', 'location', 'state',
'states', 'address'
] as const;
/**
* Países comuns para detecção automática
*/
export const COMMON_COUNTRIES = [
'brazil', 'brasil', 'usa', 'united states',
'canada', 'mexico', 'argentina', 'chile',
'colombia'
] as const;
@@ -0,0 +1,88 @@
/**
*/
export const AI_DOCUMENTATION_PROMPT = `crie uma documentação em Portugues, Ingles e Espanhol seguindo essas instruções
1. Persona: como profissional de governança e engenharia de dados
2. Tarefa: ao receber as informações da tabela criar uma documentação com o seguinte escopo
**A primeira linha do documento tem que conter a seguinte informação: ## Document languages: EN / BR / ES
**A segunda linha tem que obrigatoriamente conter a escrita Table: nome da tabela
**A terceira linha tem que obrigatoriamente conter a escrita Table Schema: nome do table schema
**DIRETRIZ CRUCIAL DE CONSISTÊNCIA E COMPLETUDE DE SCHEMA:**
**1. Fonte Exclusiva de Metadados:** O 'Table Schema' definido na linha acima é a ÚNICA fonte de verdade para o schema dos dados a serem documentados. TODAS as informações subsequentes, especialmente na seção 'Estrutura da Tabela' (incluindo a lista de colunas, seus nomes, tipos de dados, descrições e exemplos) DEVEM ser extraídas EXCLUSIVAMENTE de metadados que correspondem a ESTE 'Table Schema'. Se os dados de entrada que você recebeu contiverem informações para a mesma tabela ou colunas mas de schemas diferentes (ex: um schema 'bronze' e um 'silver'), você DEVE IGNORAR TOTALMENTE as informações dos schemas divergentes para esta tarefa de documentação e utilizar APENAS as do 'Table Schema' aqui especificado.
**2. Listagem Completa de Colunas:** Sua principal tarefa na seção 'Estrutura da Tabela' é identificar e listar TODAS as colunas que pertencem ao 'Table Schema' especificado. Verifique nos dados de entrada fornecidos se uma indicação explícita do número total de colunas para esta tabela neste schema (por exemplo, um campo como 'Num_columns' ou similar nos metadados da tabela). Você deve se esforçar para listar exatamente essa quantidade de colunas. Se essa contagem não estiver disponível, liste todas as colunas que você puder identificar como pertencentes exclusivamente a este 'Table Schema'. A completude em relação ao schema especificado é essencial.
**Depois de "Estrutura da tablea", incluir a mensagem "Este documento foi gerado por IA", traduzida corretamente para cada idioma.**
**Obrigatoriamente:Após finalizar a versão em Inglês, começar a versão em Português** **Após finalizar a versão em Português, começar a versão em Espanhol** **Antes de começar cada versão, colocar um título como:** - \`## English Version\` (para inglês)
- \`## Versão em Português\` (para português)
- \`## Versión en Español\` (para espanhol)
- Descrição: fornece uma visão geral do ativo de dados,
destacando seu propósito e principal funcionalidade.
Esta sessão resume o conteúdo e o objetivo do ativo, ajudando os usuários a entender rapidamente o que o ativo representa
e como pode ser utilizado em suas análises e decisões.
- Sugestão de Domínio de Dados:
Analise cuidadosamente os dados da tabela e sugira o domínio mais apropriado. Inclua:
- Domínio Sugerido: [Nome do domínio]
- Motivo: [Explicação breve sobre porque a tabela pertence a este domínio]
- Observações: [Qualquer observação adicional relevante]
Exemplos de Domínios de Dados para referência:
- Financeiro: Dados sobre transações, receitas, despesas, etc.
- Recursos Humanos: Dados sobre funcionários, cargos, salários, etc.
- Produtos: Dados sobre produtos, categorias, preços, etc.
- Fornecedores: Dados sobre fornecedores, produtos fornecidos, localizações, etc.
- Marketing: Dados sobre campanhas, leads, conversões, etc.
- Vendas: Dados sobre vendas, clientes, produtos vendidos, etc.
- Operações: Dados sobre processos, logística, produção, etc.
- Clientes: Dados sobre clientes, interações, histórico, etc.
-Tags Sugeridas:
A IA deve gerar tags relevantes **com base nos dados da tabela**.
- **IMPORTANTE: Analise cuidadosamente os dados de preview da tabela (PREVIEW DATA) para encontrar países. Procure em todas as colunas por nomes de países, cidades ou regiões.**
- **Garanta que as tags estejam separadas por espaços vazios, todas na mesma linha, exemplo: #marketing #sales #australia #canada, limitar até 3 países que mais aparecem** - **Os países DEVEM ser extraídos dos dados de preview da tabela. Procure em colunas como City, Country, Region, Location, etc.** - Por que esta tabela é interessante:
Nesta sessão, é destacada a importância do ativo, explicando como ele pode ser útil para os usuários.
São abordadas as formas como o ativo pode melhorar a tomada de decisões, identificar padrões relevantes ou fornecer insights valiosos.
O objetivo é ressaltar a utilidade prática e o impacto positivo que o ativo pode ter em suas atividades.
- Análises potencialmente úteis feitas com esses dados:
Aqui são listadas algumas das análises que podem ser realizadas com o ativo de dados. Inclui sugestões de dashboards,
relatórios ou outros tipos de análises que aproveitam as informações fornecidas pelo ativo.
O objetivo é oferecer maneiras de utilizar os dados para obter insights valiosos e apoiar a tomada de decisões informadas.
- Links Úteis:
Os Links Úteis oferecem recursos adicionais relacionados ao ativo de dados, incluindo guias,
artigos ou outras fontes de informação que podem ajudar os usuários a compreender melhor o ativo e suas aplicações. Além disso,
inclui um link rápido dentro da Dadosfera para ativos relacionados diretamente com o ativo em questão, facilitando a navegação entre os ativos.
- Estrutura da Tabela:
A Estrutura da Tabela detalha TODAS as colunas e os dados disponíveis no ativo, conforme pertencentes ao 'Table Schema' principal definido no início deste documento.
**Instrução Detalhada para Estrutura da Tabela:**
Siga rigorosamente estes passos:
1. Identifique nos dados de entrada (metadados da tabela e das colunas) todas as colunas que pertencem EXCLUSIVAMENTE ao 'Table Schema' especificado no cabeçalho deste documento. Se houver uma contagem de colunas (ex: 'Num_columns') para este schema específico, assegure-se de listar essa quantidade.
2. Para CADA uma dessas colunas identificadas, formate a saída da seguinte maneira, **SEM utilizar NENHUM marcador de lista (como traços ou asteriscos) no início de cada entrada de coluna**. Cada coluna deve ser apresentada como um bloco de texto. Inclua uma linha em branco entre a documentação de cada coluna para separação visual.
- Apresente o NOME_DA_COLUNA em maiúsculas, seguido pelo (TIPO_DE_DADO_EXTRAÍDO_DOS_METADADOS_DO_SCHEMA_CORRETO) entre parênteses.
- O **NOME_DA_COLUNA (TIPO_DE_DADO_EXTRAÍDO_DOS_METADADOS_DO_SCHEMA_CORRETO)** deve estar na primeira linha do bloco da coluna e **inteiramente em negrito**.
- Na linha seguinte, a etiqueta "**Descrição:**" deve estar **em negrito**, seguida pelo texto da descrição da coluna.
- Na linha seguinte à descrição, a etiqueta "**Exemplo:**" deve estar **em negrito**, seguida pelo valor do exemplo. Se o exemplo for um valor literal ou código, formate-o entre crases (\`) se apropriado.
- Se houver informações adicionais relevantes (como "Valores Possíveis:", "Observações:", etc.), coloque a etiqueta correspondente **em negrito** em uma nova linha, seguida pelo seu texto.
Este documento foi gerado por IA.
NOME_COLUNA_1 (TIPO_DADO_SCHEMA_CORRETO_1):
Descrição: [Descrição da coluna 1, do schema correto]
Exemplo: \`[Exemplo de valor para coluna 1, do schema correto]\`
NOME_COLUNA_2 (TIPO_DADO_SCHEMA_CORRETO_2):
Descrição: [Descrição da coluna 2, do schema correto]
Exemplo: \`[Exemplo de valor para coluna 2, do schema correto]\`
(continue este formato com início de cada coluna para TODAS as colunas do 'Table Schema' especificado, garanta com que NUNCA tenha TRAÇO OU PONTO no inicio)
3. Contexto : O usuário ira cadastrar um ativo de dados na nossa plataforma e para ter um bom catalogo ele ira querer gerar a documentação padronizada mas explicativa e
automática
4. Restrições : A documentação deve seguir obrigatoriamente o mesmo padrão principalmente na parte de estrutura de dados
5. Objetivo: O principal objetivo é gerar uma documentação acessível, clara,
automática e padronizada para os usuários que desejem cadastrar um ativo de dados na plataforma`;
/**
* Configurações para a geração de documentação com IA
*/
export const AI_DOCUMENTATION_CONFIG = {
FETCH_K: 250,
K: 100,
} as const;
@@ -0,0 +1,60 @@
/**
* Tipos relacionados à geração de documentação com IA
*/
export interface AutodriveCredentials {
username: string;
password: string;
baseUrl: string;
model: string;
authHeader?: string;
}
export interface AutodriveAskPayload {
question: string;
fetch_k: number;
k: number;
model: string;
}
export interface AutodriveAskResponse {
answer?: string;
question_id?: string;
dataset_id?: string;
}
export interface AutodriveAnswerResponse {
status: 'started' | 'success' | 'failed';
answer?: string;
status_reason?: string;
}
export interface AutodriveUploadResponse {
dataset_id: string;
}
export interface DatasetStatusResponse {
status: 'processing' | 'success' | 'failed';
status_reason?: string;
}
export interface ColumnData {
name: string;
type: string;
description?: string;
nullable?: string;
}
export interface ColumnsMetadata {
columns: ColumnData[];
}
export interface DataPreview {
preview: any[];
}
export interface FormattedDataForAI {
dataAsset: any;
dataPreview: any[];
columnsData?: ColumnsMetadata;
}