Inclusão do projeto dcat-br-ecossistema

parent 8a6553c6
# 🗃️ Dcat-br-ecossistema - DCAT-BR
[![Python](https://img.shields.io/badge/Python-3.8%2B-blue?logo=python)](https://www.python.org/)
[![Flask](https://img.shields.io/badge/Flask-3.0-blue?logo=flask)](https://flask.palletsprojects.com/)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
Conjunto de ferramentas oficial para catalogação de conjuntos de dados governamentais brasileiros seguindo o padrão **DCAT-BR**, com geração automática de metadados no padrão DCAT-BR.
![Interface Preview](https://www.gov.br/governodigital/pt-br/infraestrutura-nacional-de-dados/catalogo-nacional-de-dados/catalogo_nacional_de_dados/@@govbr.institucional.banner/b1aa34c9-9395-41d2-8978-21ebb141e44b/@@images/9c458d27-e88b-4978-bd57-b4f5c38880fb.png)
## 📝 Acessar solução
- [Cataloador - DCAT-BR](https://www.gov.br/governodigital/pt-br/infraestrutura-nacional-de-dados/catalogo-nacional-de-dados)
## ✨ Funcionalidades
- **Preenchimento Assistido por IA**
- 🤖 Agente de IA que auxilia o preenchimento supervisionado de metadados de conjuntos de dados
- **Validação de RDF/XML**
- 🌐 Verificação de conformidade com DCAT-BR e suporte a URIs e vocabulários controlados
- **Interface Multi-página**
- ✅ Formulário de catalogação inteligente
- 🔍 Validador de arquivos RDF
- Painel de gestão de metadados
- **Arquitetura SOA**
Serviços independentes para validação, geração e persistência com APIs com documentação Swagger
- **Exportação Padronizada**
📦
- RDF/XML
- JSON-LD
- CSV para sistemas legados
## ⚡ Instalação Rápida
### Pré-requisitos
- Python 3.10+
- pip
- Chave de API do Google Gemini ([obter chave](https://ai.google.dev/))
- Git (opcional)
### Passo a Passo
**Clonar o repositório**:
```bash
git clone http://git.planejamento.gov.br/sgd_publico/catalogo_nacional_dados.git
cd catalogo_nacional_dados/dcat-br-ecossistema
```
**Criar ambiente virtual:**:
```bash
python -m venv venv-dcat-br-ecossistema
```
**Ativar ambiente virtual:**
```bash
# Linux/Mac
source venv-dcat-br-ecossistema/bin/activate
# Windows
.\venv-dcat-br-ecossistema\Scripts\activate
```
**Instalar dependências:**:
```bash
pip install -r requirements.txt
```
**Configurar ambiente**
```bash
cp .env.example .env
```
**Editar .env com sua chave da API**
```bash
nano .env
```
**Executar aplicação:**:
```bash
python dcat-br-ecossistema.py
```
**Acessar interface:**:
```bash
http://localhost:5000
```
## ⚡ Como Utilizar
```bash
http://localhost:5000/catalogacao
```
- Preencha o formulário manualmente ou usando o assistente de IA;
- Para campos URI, utilize endereços válidos (ex: http://creativecommons.org/licenses/by/4.0/);
- Separe múltiplos valores (como palavras-chave) por vírgulas;
- Clique em "Gerar RDF" para exportar o arquivo de metadados.
```bash
http://localhost:5000/validacao
```
- Clique no botão 🤖 no formulário;
- Forneça contexto básico;
- Revise e ajuste as sugestões automáticas.
## 🤝 Como Contribuir
- Faça um fork do projeto;
- Crie uma branch para sua feature:
**Feature**:
```bash
git checkout -b feature/nova-feature
```
**Commit suas mudanças**:
```bash
git commit -m 'Adiciona nova funcionalidade'
```
**Push para a branch:**:
```bash
git push origin feature/nova-feature
```
**Abra um Pull Request**:
## 📄 Licença
- Este projeto está licenciado sob a Licença MIT.
## 🌐 Recursos Oficiais
- Documentação DCAT-BR
- W3C DCAT
## 👥 Reconhecimentos
- W3C DCAT
GEMINI_API_KEY=AIzaSyCNza7OV5r7EcgSD7uYL4E53x83gbuR_NU
FLASK_ENV=development
\ No newline at end of file
from flask import request, jsonify
from app.services.rdf_generation_service import RDFGenerator
from app.services.rdf_parsing_service import RDFParser
from app.services.validation_service import ValidationService
class DcatController:
@staticmethod
def generate_rdf():
required_fields = ['title', 'description', 'publisher', 'contactPoint',
'license', 'accrualPeriodicity']
form_data = request.form if request.form else request.get_json()
is_valid, message = ValidationService.validate_form_data(form_data, required_fields)
if not is_valid:
return jsonify({"error": message}), 400
rdf_content = RDFGenerator.generate(form_data)
return rdf_content, 200, {
'Content-Type': 'application/rdf+xml',
'Content-Disposition': 'attachment; filename=metadata.rdf'
}
@staticmethod
def import_rdf():
if 'rdfFile' not in request.files:
return jsonify({"error": "Nenhum arquivo enviado"}), 400
file = request.files['rdfFile']
if not file or file.filename == '':
return jsonify({"error": "Nome de arquivo inválido"}), 400
try:
extracted_data = RDFParser.parse_rdf_file(file.read())
return jsonify(extracted_data)
except Exception as e:
return jsonify({"error": f"Erro ao processar RDF: {str(e)}"}), 400
@staticmethod
def validate_rdf():
if 'rdfFile' not in request.files:
return jsonify({"valid": False, "errors": ["Nenhum arquivo enviado"]}), 400
file = request.files['rdfFile']
if not file or file.filename == '':
return jsonify({"valid": False, "errors": ["Nome de arquivo inválido"]}), 400
try:
is_valid, errors = ValidationService.validate_rdf_file(file.read())
return jsonify({
"valid": is_valid,
"errors": errors
}), 200 if is_valid else 400
except Exception as e:
return jsonify({
"valid": False,
"errors": [f"Erro interno: {str(e)}"]
}), 500
\ No newline at end of file
def save_rdf_to_file(rdf_content, filename="metadata.rdf"):
with open(filename, "w") as f:
f.write(rdf_content)
return filename
\ No newline at end of file
from rdflib import Graph, Namespace, URIRef, Literal
from rdflib.namespace import DCTERMS, FOAF, RDF, XSD # ADMS removido daqui
import uuid
# Defina manualmente os namespaces ausentes
ADMS = Namespace("http://www.w3.org/ns/adms#")
DCAT = Namespace("http://www.w3.org/ns/dcat#")
VCARD = Namespace("http://www.w3.org/2006/vcard/ns#")
class RDFGenerator:
@staticmethod
def generate(form_data):
g = Graph()
g = RDFGenerator._bind_namespaces(g)
dataset_id = RDFGenerator._create_dataset(g, form_data)
RDFGenerator._add_creator(g, dataset_id, form_data)
RDFGenerator._add_contact_point(g, dataset_id, form_data)
RDFGenerator._add_keywords_and_themes(g, dataset_id, form_data)
return g.serialize(format='pretty-xml')
@staticmethod
def _bind_namespaces(graph):
namespaces = {
"dcterms": DCTERMS,
"dcat": DCAT,
"foaf": FOAF,
"vcard": VCARD,
"adms": ADMS
}
for prefix, namespace in namespaces.items():
graph.bind(prefix, namespace)
return graph
@staticmethod
def _create_dataset(graph, data):
dataset_id = URIRef(f"https://dados.gov.br/dados/conjuntos-dados/{uuid.uuid4()}")
graph.add((dataset_id, RDF.type, DCAT.Dataset))
properties = [
(DCTERMS.title, data['title']),
(DCTERMS.description, data['description']),
(DCTERMS.publisher, data['publisher']),
(DCTERMS.license, URIRef(data['license'])),
(DCTERMS.accrualPeriodicity, data['accrualPeriodicity']),
(DCTERMS.accessRights, data['accessRights']),
(ADMS.version, data.get('version', '1.0'))
]
for pred, value in properties:
graph.add((dataset_id, pred, Literal(value)))
return dataset_id
@staticmethod
def _add_creator(graph, dataset_id, data):
creator_id = URIRef(f"https://dados.gov.br/dados/organizacoes/{uuid.uuid4()}")
graph.add((dataset_id, DCTERMS.creator, creator_id))
graph.add((creator_id, RDF.type, FOAF.Organization))
graph.add((creator_id, FOAF.name, Literal(data['creator'])))
@staticmethod
def _add_contact_point(graph, dataset_id, data):
contact_id = URIRef(f"mailto:{data['contactPoint']}")
graph.add((dataset_id, DCAT.contactPoint, contact_id))
graph.add((contact_id, RDF.type, VCARD.Organization))
graph.add((contact_id, VCARD.hasEmail, contact_id))
@staticmethod
def _add_keywords_and_themes(graph, dataset_id, data):
for field, predicate in [('keywords', DCAT.keyword), ('theme', DCAT.theme)]:
if data.get(field):
for item in data[field].split(','):
graph.add((dataset_id, predicate, Literal(item.strip())))
\ No newline at end of file
from rdflib import Graph, Namespace
from rdflib.namespace import DCAT, DCTERMS, FOAF, RDF, XSD
ADMS = Namespace("http://www.w3.org/ns/adms#")
DCAT = Namespace("http://www.w3.org/ns/dcat#")
DCAT.license = Namespace("http://purl.org/dc/terms/license")
class RDFParser:
@staticmethod
def parse_rdf_file(file_content):
g = Graph()
g.bind("dcat", DCAT)
g.bind("dcterms", DCTERMS)
g.parse(data=file_content, format='xml')
dataset = next(g.subjects(RDF.type, DCAT.Dataset), None)
if not dataset:
raise ValueError("Nenhum dataset DCAT encontrado no arquivo")
creator = g.value(dataset, DCTERMS.creator)
return {
'title': str(g.value(dataset, DCTERMS.title)),
'description': str(g.value(dataset, DCTERMS.description)),
'publisher': str(g.value(dataset, DCTERMS.publisher)),
'contactPoint': str(g.value(dataset, DCAT.contactPoint)).replace('mailto:', ''),
'license': str(g.value(dataset, DCTERMS.license)),
'accrualPeriodicity': str(g.value(dataset, DCTERMS.accrualPeriodicity)),
'accessRights': str(g.value(dataset, DCTERMS.accessRights)),
'version': str(g.value(dataset, ADMS.version)),
'keywords': ', '.join(str(k) for k in g.objects(dataset, DCAT.keyword)),
'theme': ', '.join(str(t) for t in g.objects(dataset, DCAT.theme)),
'creator': str(g.value(creator, FOAF.name)) if creator else ''
}
from rdflib import Graph
from rdflib.namespace import DCAT, DCTERMS, FOAF, RDF, XSD
from urllib.parse import urlparse
from flask import jsonify
from jsonschema import validate
from jsonschema.exceptions import ValidationError
class ValidationService:
@staticmethod
def validate_form_data(form_data, required_fields):
errors = []
# Validação de campos obrigatórios
for field in required_fields:
if not form_data.get(field):
errors.append(f"Campo obrigatório faltando: {field}")
# Validação de e-mail
if '@' not in form_data.get('contactPoint', ''):
errors.append("Formato de e-mail inválido no campo de contato")
# Validação de URL da licença
try:
url = urlparse(form_data['license'])
if not all([url.scheme, url.netloc]):
errors.append("URL da licença inválida")
except:
errors.append("Erro na análise da URL da licença")
return len(errors) == 0, errors if errors else "Validação bem-sucedida"
@staticmethod
def validate_rdf_file(file_content):
try:
g = Graph()
g.parse(data=file_content, format='xml')
errors = []
dataset = next(g.subjects(RDF.type, DCAT.Dataset), None)
# Validação de propriedades obrigatórias DCAT-BR
required_properties = [
(DCTERMS.title, "Título"),
(DCTERMS.description, "Descrição"),
(DCTERMS.publisher, "Publicador"),
(DCAT.contactPoint, "Ponto de contato"),
(DCTERMS.license, "Licença")
]
for prop, name in required_properties:
if not g.value(dataset, prop):
errors.append(f"Propriedade obrigatória ausente: {name} ({prop})")
# Validação de formato de e-mail
contact_point = str(g.value(dataset, DCAT.contactPoint))
if not contact_point.startswith("mailto:") or '@' not in contact_point:
errors.append("Formato inválido para ponto de contato")
return True if not errors else False, errors
except Exception as e:
return False, [f"Erro na análise do RDF: {str(e)}"]
from flask import Flask, render_template, jsonify, request
from dotenv import load_dotenv
from flasgger import Swagger
from app.controllers.dcat_controller import DcatController
from google.generativeai import configure, GenerativeModel
from google.api_core import exceptions as google_exceptions
import os
import json
import google.generativeai as genai
app = Flask(__name__)
# Configuração do Swagger
SWAGGER_CONFIG = {
"headers": [],
"specs": [
{
"endpoint": "apispec",
"route": "/api/docs/apispec.json",
"rule_filter": lambda rule: True,
"model_filter": lambda tag: True,
}
],
"static_url_path": "/flasgger_static",
"swagger_ui": True,
"specs_route": "/api/docs/",
"title": "API DCAT-BR",
}
swagger = Swagger(app, config=SWAGGER_CONFIG) # Configuração explícita
@app.route('/')
def index():
return render_template('index.html', active_page='index')
@app.route('/catalogacao')
def catalogacao():
return render_template('catalogador.html', active_page='catalogacao')
@app.route('/validacao')
def validacao():
return render_template('validacao.html', active_page='validacao')
@app.route('/generate_rdf', methods=['POST'])
def generate_rdf():
"""Endpoint para geração de RDF
---
tags:
- DCAT
consumes:
- application/x-www-form-urlencoded
parameters:
- name: title
in: formData
type: string
required: true
- name: description
in: formData
type: string
required: true
- name: publisher
in: formData
type: string
required: true
- name: contactPoint
in: formData
type: string
format: email
required: true
- name: license
in: formData
type: string
format: uri
required: true
- name: accrualPeriodicity
in: formData
type: string
required: true
- name: accessRights
in: formData
type: string
required: true
- name: version
in: formData
type: string
- name: keywords
in: formData
type: string
- name: theme
in: formData
type: string
required: true
- name: creator
in: formData
type: string
required: true
responses:
200:
description: Arquivo RDF gerado
content:
application/rdf+xml:
schema:
type: string
example: |
<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF>...</rdf:RDF>
400:
description: Erro de validação
"""
return DcatController.generate_rdf()
@app.route('/import_rdf', methods=['POST'])
def import_rdf():
"""Endpoint para importação de RDF
---
tags:
- DCAT
consumes:
- multipart/form-data
parameters:
- name: rdfFile
in: formData
type: file
required: true
responses:
200:
description: Dados extraídos do RDF
schema:
type: object
properties:
title: {type: string}
description: {type: string}
#... todos os outros campos
400:
description: Erro na importação
"""
return DcatController.import_rdf()
@app.route('/validate_rdf', methods=['POST'])
def validate_rdf():
"""Validação de arquivo RDF DCAT-BR
---
tags:
- Validação
consumes:
- multipart/form-data
parameters:
- name: rdfFile
in: formData
type: file
required: true
responses:
200:
description: Resultado da validação
schema:
type: object
properties:
valid:
type: boolean
errors:
type: array
items: string
"""
return DcatController.validate_rdf()
load_dotenv() # Carregar variáveis do .env
@app.route('/generate-with-ai', methods=['POST'])
def generate_with_ai():
try:
# Verificação básica dos dados
data = request.get_json()
if not data or 'description' not in data or 'orgao' not in data:
return jsonify({"error": "Dados incompletos"}), 400
# Configuração da API
genai.configure(
api_key=os.getenv("GEMINI_API_KEY"),
transport='rest'
)
#for model in genai.list_models():
# print(model.name)
# Criação do modelo
#model = genai.GenerativeModel('gemini-1.5-flash')
#model = genai.GenerativeModel('gemini-2.0-pro-exp')
model = genai.GenerativeModel('gemini-2.0-flash')
#"publisher": {"type": "string", "pattern": "Ministério|Instituto|Agência"},
template = {
"title": {"type": "string", "max_length": 100},
"description": {"type": "string", "min_length": 250},
"accrualPeriodicity": {"enum": ["Diária", "Semanal", "Mensal", "Anual"]},
"keywords": {"type": "array", "items": {"type": "string"}},
"contactPoint": {"type": "string", "max_length": 20},
"publisher": {"type": "string", "max_length": 20},
"version": {"type": "string", "max_length": 10},
"accessRights": {"type": "uri", "format": "url"},
"theme": {"type": "array", "items": {"type": "string"}},
"license": {"type": "uri", "format": "url"},
"creator": {"type": "string", "max_length": 50}
}
# Prompt estruturado
prompt = f"""
**Instruções DCAT-BR:**
Gere metadados em JSON para os seguintes inputs:
- Descrição: {data['description'][:1000]}
- Órgão: {data['orgao']}
Regras:
1. Gerar JSON estritamente válido;
2. Seguir template: {json.dumps(template)};
3. Campos desconhecidos deve ser preenchido como "string" vazia;
4. Formatar segundo Manual DCAT-BR;
5. Validar tipos de dados;
6. Usar vocabulários controlados;
7. Seguir os padrão DCAT Versão 3 da W3C;
8. Fazer uso de URI válidas;
10. Para o campo "accessRights" usar URL da lei vigente publicada no "Diário Oficial" ou arcabouço legal relacionado ao conjunto de dados que está sendo catalogado.
Resposta APENAS em JSON:
"""
# Chamada à API
response = model.generate_content(
prompt,
generation_config={
"temperature": 0.1,
"top_p": 0.95,
"top_k": 40,
"max_output_tokens": 2000
},
safety_settings={
"HARASSMENT": "BLOCK_NONE",
"HATE": "BLOCK_NONE",
"SEXUAL": "BLOCK_NONE",
"DANGEROUS": "BLOCK_NONE"
}
)
# Processamento da resposta
try:
cleaned_response = response.text.replace('```json', '').replace('```', '').strip()
result = json.loads(cleaned_response)
#print(result)
return jsonify(result)
except json.JSONDecodeError:
return jsonify({
"error": "Resposta em formato inválido",
"raw_response": response.text
}), 500
except Exception as e:
return jsonify({
"error": "Erro interno - " + str(e),
"details": str(e)
}), 500
#@app.route('/static/<path:path>')
#def send_static(path):
# return send_from_directory('static', path)
if __name__ == '__main__':
app.run(debug=True)
\ No newline at end of file
flask==2.0.3
flasgger==0.9.5
rdflib==6.3.2
werkzeug==2.0.3
google-generativeai==0.3.2
python-dotenv==0.19.0
uuid==1.30
\ No newline at end of file
/* Estilos Base */
body {
font-family: 'Public Sans', sans-serif;
background-color: #f8f9fa;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Cabeçalho Governamental */
.gov-header {
background-color: #0f3866;
border-bottom: 4px solid #f5c400;
color: white;
padding: 1rem 0;
}
.gov-logo {
height: 50px;
}
/* Navegação */
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
}
.nav-link {
color: #0f3866 !important;
padding: 0.5rem 1rem !important;
transition: all 0.3s ease;
}
.nav-link.active {
font-weight: 600;
border-bottom: 3px solid #f5c400 !important;
}
/* Cards de Serviço */
.service-card {
transition: transform 0.3s;
border: 1px solid rgba(0,0,0,.125);
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15);
}
/* Formulários */
.required-label::after {
content: "*";
color: #dc3545;
margin-left: 3px;
}
.form-control:focus {
border-color: #0f3866;
box-shadow: 0 0 0 0.25rem rgba(15, 56, 102, 0.25);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.was-validated .form-control:invalid ~ .invalid-feedback,
.was-validated .form-control:invalid ~ .invalid-tooltip {
display: block;
}
/* Validação */
.validation-alert {
border-left: 4px solid transparent;
padding: 1rem;
margin: 1rem 0;
}
.validation-alert.success {
border-color: #28a745;
background-color: #d4edda;
}
.validation-alert.error {
border-color: #dc3545;
background-color: #f8d7da;
}
/* Conteúdo Principal */
.main-content {
flex: 1;
padding: 2rem 0;
}
/* Rodapé */
.gov-footer {
background-color: #0f3866;
color: white;
padding: 1.5rem 0;
margin-top: auto;
}
.footer-links a {
color: #f5c400;
text-decoration: none;
}
.footer-links a:hover {
text-decoration: underline;
}
/* Botão IA */
.btn-ai {
background-color: #2d7ff9;
color: white;
border-radius: 25px;
padding: 12px 25px;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
border: none;
}
.btn-ai:hover {
background-color: #1a5fb4;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(45, 127, 249, 0.3);
}
.bi-robot {
font-size: 1.2rem;
vertical-align: middle;
}
.btn-ai:hover {
background-color: #357abd;
}
/* Wizard de IA */
#aiWizard .modal-content {
border-radius: 10px;
}
#aiStatus {
transition: all 0.3s ease;
}
/* Select2 no modal */
.select2-container {
z-index: 9999 !important; /* Garante visibilidade no modal */
}
.select2-results__option {
padding: 8px 12px;
}
.select2-results__option--highlighted {
background-color: #2d7ff9 !important;
color: white !important;
}
.select2-search--dropdown .select2-search__field {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 6px;
}
/* Reset de estado de campos */
#aiWizard .form-control:focus {
box-shadow: none;
border-color: #ced4da;
}
#aiWizard .was-validated .form-control:valid {
border-color: #ced4da;
background-image: none;
}
\ No newline at end of file
/*let siorgData = [];
async function loadSIORGData() {
siorgData = [
{ codigo: "001", nome: "Presidência da República" },
{ codigo: "002", nome: "Ministério da Economia" },
{ codigo: "003", nome: "Ministério da Saúde" },
{ codigo: "004", nome: "Ministério da Educação" },
{ codigo: "005", nome: "Ministério da Defesa" },
{ codigo: "006", nome: "Ministério da Justiça e Segurança Pública" },
{ codigo: "007", nome: "Banco Central do Brasil" },
{ codigo: "008", nome: "Instituto Brasileiro de Geografia e Estatística (IBGE)" },
{ codigo: "009", nome: "Instituto Nacional de Seguridade Social (INSS)" },
{ codigo: "010", nome: "Agência Nacional de Saúde Suplementar (ANS)" },
{ codigo: "011", nome: "Agência Nacional de Águas e Saneamento Básico (ANA)" },
{ codigo: "012", nome: "Instituto do Patrimônio Histórico e Artístico Nacional (IPHAN)" },
{ codigo: "013", nome: "Agência Nacional de Telecomunicações (ANATEL)" },
{ codigo: "014", nome: "Agência Nacional de Aviação Civil (ANAC)" },
{ codigo: "015", nome: "Instituto Nacional de Metrologia, Qualidade e Tecnologia (INMETRO)" }
];
initSelect2();
Api do SIORG
try {
const response = await fetch('https://api.portaldatransparencia.gov.br/siorg/v1/orgaos');
siorgData = await response.json();
initSelect2();
} catch (error) {
console.error('Erro ao carregar SIORG:', error);
}
}
*/
const siorgData = [
{ id: 1, text: "Ministério da Saúde" },
{ id: 2, text: "Ministério da Educação" },
{ id: 3, text: "Instituto Brasileiro de Geografia e Estatística (IBGE)" },
{ id: 4, text: "Agência Nacional de Saúde Suplementar (ANS)" },
{ id: 5, text: "Banco Central do Brasil" },
{ id: 6, text: "Ministério da Cidadania" }
];
function initSelect2() {
$('#aiOrgao').select2({
data: siorgData,
placeholder: "Digite para buscar...",
minimumInputLength: 2,
language: {
noResults: () => "Nenhum órgão encontrado",
searching: () => "Buscando...",
inputTooShort: () => "Digite pelo menos 2 caracteres"
},
dropdownParent: $('#aiWizard') // Importante para modais
});
}
function openAIWizard() {
$('#aiWizard').modal('show');
setTimeout(initSelect2, 500); // Garante inicialização após abertura do modal
hideAIStatus();
}
async function generateWithAI() {
const description = document.getElementById('aiDescription').value;
const orgao = $('#aiOrgao').select2('data')[0]?.text || '';
//console.log(description + " - " + orgao);
if (!description || description.length < 10) {
showAIStatus('A descrição deve ter pelo menos 10 caracteres', 'error');
return;
}
showAIStatus('Gerando metadados por IA... Analise os metadados antes de gerar o RDF.', 'loading');
//const status = document.getElementById('aiStatus');
//status.classList.remove('d-none');
//status.innerHTML = '<div class="spinner-border spinner-border-sm"></div> Gerando metadados...';
try {
const response = await fetch('/generate-with-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description, orgao })
});
const data = await response.json();
if (data.error) throw new Error(data.error);
fillFormWithAI(data);
$('#aiWizard').modal('hide');
//} catch (error) {
// status.innerHTML = `<div class="text-danger">Erro: ${error.message}</div>`;
//}
} catch (error) {
showAIStatus(`Erro: ${error.message}`, 'error');
}
}
function showAIStatus(message, type = 'info') {
const status = document.getElementById('aiStatus');
status.className = `alert alert-${type === 'error' ? 'danger' : 'info'}`;
status.innerHTML = type === 'loading'
? `<div class="spinner-border spinner-border-sm"></div> ${message}`
: message;
}
function fillFormWithAI(data) {
const mappings = {
title: '',
description: '',
accrualPeriodicity:'',
keywords: '',
contactPoint: '',
publisher: '',
version: '',
accessRights: '',
theme: '',
license: '',
creator: '',
// ... outros mapeamentos
};
Object.entries(mappings).forEach(([field, value]) => {
const element = document.querySelector(`[name="${field}"]`);
if (element) element.value = data[field] || value || 'Informação não encontrada pela IA';
});
}
function clearAIForm() {
// Limpar campos do formulário
document.getElementById('aiDescription').value = '';
// Resetar o Select2
$('#aiOrgao').val(null).trigger('change');
hideAIStatus();
}
function hideAIStatus() {
const statusDiv = document.getElementById('aiStatus');
statusDiv.classList.add('d-none'); // Oculta usando Bootstrap
statusDiv.innerHTML = ''; // Limpa conteúdo anterior
// Opcional: Remover estilos específicos se houver
statusDiv.style.removeProperty('background-color');
statusDiv.style.removeProperty('border-color');
//if (abortController) {
// abortController.abort();
//}
}
// Validação de formulário
(() => {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
// Função de importação de RDF
function importRDF() {
const fileInput = document.getElementById('rdfFile')
const errorDiv = document.getElementById('importError')
if (!fileInput.files[0]) {
errorDiv.textContent = "Selecione um arquivo RDF primeiro!"
return
}
const formData = new FormData()
formData.append('rdfFile', fileInput.files[0])
fetch('/import_rdf', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
errorDiv.textContent = data.error
return
}
// Preenche os campos do formulário
Object.keys(data).forEach(key => {
const element = document.querySelector(`[name="${key}"]`)
if (element) element.value = data[key]
})
errorDiv.textContent = ""
})
.catch(error => {
errorDiv.textContent = "Erro ao processar o arquivo RDF"
})
}
function validateRDF() {
const fileInput = document.getElementById('rdfValidationFile');
const resultDiv = document.getElementById('validationResult');
if (!fileInput.files[0]) {
resultDiv.innerHTML = `<div class="alert alert-danger">Selecione um arquivo RDF</div>`;
return;
}
const formData = new FormData();
formData.append('rdfFile', fileInput.files[0]);
fetch('/validate_rdf', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.valid) {
resultDiv.innerHTML = `<div class="alert alert-success">✓ Arquivo válido conforme DCAT-BR</div>`;
} else {
const errors = data.errors.map(e => `<li>${e}</li>`).join('');
resultDiv.innerHTML = `
<div class="alert alert-danger">
<strong>Erros encontrados:</strong>
<ul>${errors}</ul>
</div>
`;
}
})
.catch(error => {
resultDiv.innerHTML = `<div class="alert alert-danger">Erro na validação</div>`;
});
}
\ No newline at end of file
function validateRDF() {
const fileInput = document.getElementById('rdfFile');
const resultDiv = document.getElementById('validationResult');
if (!fileInput.files[0]) {
resultDiv.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-circle"></i> Selecione um arquivo RDF primeiro!
</div>`;
return;
}
const formData = new FormData();
formData.append('rdfFile', fileInput.files[0]);
resultDiv.innerHTML = `
<div class="alert alert-info">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Validando...</span>
</div>
Validando arquivo...
</div>`;
fetch('/validate_rdf', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.valid) {
resultDiv.innerHTML = `
<div class="alert alert-success">
<i class="bi bi-check-circle"></i> Arquivo válido!
<div class="mt-2">O arquivo está em conformidade com o padrão DCAT-BR</div>
</div>`;
} else {
const errors = data.errors.map(e => `
<li class="list-group-item d-flex justify-content-between align-items-start">
<div class="ms-2 me-auto">
<div class="fw-bold">${e.split(':')[0]}</div>
${e.split(':')[1] || ''}
</div>
<span class="badge bg-danger rounded-pill">!</span>
</li>
`).join('');
resultDiv.innerHTML = `
<div class="alert alert-danger">
<h5><i class="bi bi-x-circle"></i> Erros encontrados:</h5>
<ul class="list-group mt-3">${errors}</ul>
</div>`;
}
})
.catch(error => {
resultDiv.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-x-circle"></i> Erro na validação: ${error.message}
</div>`;
});
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Portal DCAT-BR - Governo Federal{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
{% block extra_css %}{% endblock %}
</head>
<body>
<header class="gov-header">
<div class="container">
<div class="row align-items-center">
<div class="col-auto">
<img src="https://www.gov.br/++theme++padrao_govbr/img/govbr-colorido-b.png"
alt="Governo Federal"
class="gov-logo">
</div>
<div class="col">
<h1 class="h4 mb-0">{% block header_title %}Portal DCAT-BR{% endblock %}</h1>
<p class="mb-0">{% block header_subtitle %}Catálogo Nacional de Dados Abertos{% endblock %}</p>
</div>
</div>
</div>
</header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if active_page == 'index' %}active{% endif %}" href="/">Página Inicial</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'catalogacao' %}active{% endif %}" href="/catalogacao">Catalogação</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'validacao' %}active{% endif %}" href="/validacao">Validação</a>
</li>
</ul>
</div>
</nav>
<main class="main-content">
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
<footer class="gov-footer">
<div class="container">
<div class="row">
<div class="col-md-6 footer-links">
<h5>Links Úteis</h5>
<a href="https://www.gov.br/dadosabertos" target="_blank">Catalogo Nacional Dados</a><br>
<a href="https://www.gov.br/governodigital" target="_blank">Governo Digital</a>
</div>
<div class="col-md-6 text-end">
<p class="mb-0">© 2025 Governo Federal do Brasil</p>
<p class="mb-0">Versão 1.0.0</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
{% endblock %}
{% block title %}Catalogador DCAT-BR{% endblock %}
{% block header_title %}Catalogador DCAT-BR{% endblock %}
{% block header_subtitle %}Geração de Metadados para Conjuntos de Dados{% endblock %}
{% block content %}
<div class="my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<form action="/generate_rdf" method="POST" class="needs-validation" novalidate>
<!-- Botão IA de IA -->
<div class="mb-4 d-flex justify-content-end">
<button type="button" class="btn btn-ai" onclick="openAIWizard()">
<i class="bi bi-robot me-2"></i> Agente IA Catalogador
</button>
</div>
<!-- Wizard de IA -->
<div class="modal fade" id="aiWizard" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Agente de IA para Catalogação</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Descrição do Conjunto de Dados</label>
<textarea class="form-control" id="aiDescription" rows="4"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Órgão Responsável</label>
<select class="form-control" id="aiOrgao" style="width: 100%">
<option></option> <!-- Placeholder para Select2 -->
</select>
</div>
<div id="aiStatus" class="alert alert-info d-none"></div>
</div>
<div class="modal-footer">
<!-- Botão Limpar -->
<button type="button" class="btn btn-secondary" onclick="clearAIForm()">
<i class="bi bi-eraser me-2"></i> Limpar
</button>
<!-- Botão Gerar -->
<button type="button" class="btn btn-primary" onclick="generateWithAI()">
<i class="bi bi-magic me-2"></i> Gerar
</button>
</div>
</div>
</div>
</div>
<!-- Importar arquivo RDF -->
<div class="mb-3">
<label for="rdfFile" class="form-label">Importar RDF Existente</label>
<input type="file" class="form-control" id="rdfFile" accept=".rdf,.xml">
<button type="button" class="btn btn-secondary mt-2" onclick="importRDF()">
<i class="bi bi-upload"></i> Importar RDF
</button>
<div id="importError" class="text-danger mt-2"></div>
</div>
<!-- Título -->
<div class="mb-3">
<label for="title" class="form-label required-label">Título (dcterms:title)</label>
<input type="text" class="form-control" id="title" name="title" required
placeholder="Título do Conjunto de Dados">
<div class="invalid-feedback">Campo obrigatório.</div>
</div>
<!-- Descrição -->
<div class="mb-3">
<label for="description" class="form-label required-label">Descrição (dcterms:description)</label>
<textarea class="form-control" id="description" name="description" rows="3" required
placeholder="Descrição detalhada"></textarea>
<div class="invalid-feedback">Campo obrigatório.</div>
</div>
<!-- Periodicidade de Atualização -->
<div class="mb-3">
<label for="accrualPeriodicity" class="form-label required-label">Periodicidade (dcterms:accrualPeriodicity)</label>
<select class="form-select" id="accrualPeriodicity" name="accrualPeriodicity" required>
<option value="http://purl.org/cld/freq/daily">Diária</option>
<option value="http://purl.org/cld/freq/weekly">Semanal</option>
<option value="http://purl.org/cld/freq/monthly">Mensal</option>
<option value="http://purl.org/cld/freq/annual">Anual</option>
<option value="SOB_DEMANDA">Sob Demanda</option>
</select>
</div>
<!-- Palavras-chave -->
<div class="mb-3">
<label for="keywords" class="form-label">Palavras-chave (dcat:keyword)</label>
<input type="text" class="form-control" id="keywords" name="keywords"
placeholder="Separe por vírgulas">
</div>
<!-- E-mail da Área Técnica -->
<div class="mb-3">
<label for="contactPoint" class="form-label required-label">E-mail (dcat:contactPoint)</label>
<input type="email" class="form-control" id="contactPoint" name="contactPoint" required
placeholder="contato@orgao.gov.br">
<div class="invalid-feedback">Insira um e-mail válido.</div>
</div>
<!-- Área Técnica Responsável -->
<div class="mb-3">
<label for="publisher" class="form-label required-label">Área Técnica (dcterms:publisher)</label>
<input type="text" class="form-control" id="publisher" name="publisher" required
placeholder="Órgão Responsável">
<div class="invalid-feedback">Campo obrigatório.</div>
</div>
<!-- Versão -->
<div class="mb-3">
<label for="version" class="form-label">Versão (adms:version)</label>
<input type="text" class="form-control" id="version" name="version"required
placeholder="Ex: 1.0.0">
</div>
<!-- Observância Legal -->
<div class="mb-3">
<label for="accessRights" class="form-label required-label">Observância Legal (dcterms:accessRights)</label>
<input type="text" class="form-control" id="accessRights" name="accessRights" required
placeholder="Ex: https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm">
<div class="invalid-feedback">Campo obrigatório.</div>
</div>
<!-- Temas -->
<div class="mb-3">
<label for="theme" class="form-label required-label">Temas (dcat:theme)</label>
<input type="text" class="form-control" id="theme" name="theme"
placeholder="Separe por vírgulas" required>
<div class="invalid-feedback">Campo obrigatório.</div>
</div>
<!-- Licença -->
<div class="mb-3">
<label for="license" class="form-label required-label">Licença (dcterms:license)</label>
<input type="url" class="form-control" id="license" name="license"
placeholder="Ex: https://creativecommons.org/licenses/by/4.0/" required>
<div class="invalid-feedback">Insira uma URI válida.</div>
</div>
<!-- Organização -->
<div class="mb-3">
<label for="creator" class="form-label required-label">Organização (dcterms:creator)</label>
<input type="text" class="form-control" id="creator" name="creator" required
placeholder="Ex: Ministério da Saúde, Ministério da Educação">
<div class="invalid-feedback">Campo obrigatório.</div>
</div>
<!-- Botões -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<button type="reset" class="btn btn-outline-secondary">Limpar</button>
<button type="submit" class="btn btn-primary">Gerar RDF</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="{{ url_for('static', filename='js/ai-assistant.js') }}"></script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Portal DCAT-BR - Página Inicial{% endblock %}
{% block content %}
<div class="my-5">
<div class="row row-cols-1 row-cols-md-2 g-4">
<div class="col">
<div class="card h-100 service-card">
<div class="card-body">
<h5 class="card-title text-primary">📥 Catalogador de Conjunto Dados</h5>
<p class="card-text">Gerar metadados DCAT-BR para novos conjuntos de dados</p>
<a href="/catalogacao" class="btn btn-primary">Acessar Ferramenta</a>
</div>
</div>
</div>
<div class="col">
<div class="card h-100 service-card">
<div class="card-body">
<h5 class="card-title text-success">✅ Validador de RDF</h5>
<p class="card-text">Validar arquivos RDF no padrão DCAT-BR</p>
<a href="/validacao" class="btn btn-success">Validar Arquivo</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Validador DCAT-BR{% endblock %}
{% block header_title %}Validador DCAT-BR{% endblock %}
{% block header_subtitle %}Verificação de conformidade de metadados{% endblock %}
{% block content %}
<div class="my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Validação de Arquivo RDF</h5>
</div>
<div class="card-body">
<div class="mb-4">
<label for="rdfFile" class="form-label">Selecione o arquivo RDF/XML</label>
<input type="file" class="form-control" id="rdfFile" accept=".rdf,.xml">
</div>
<button class="btn btn-success w-100" onclick="validateRDF()">
<i class="bi bi-shield-check"></i> Validar Arquivo
</button>
<div id="validationResult" class="mt-4"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/validation.js') }}"></script>
{% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment