Amazon CodeWhisperer el asistente de programación de Amazon

0
654
Foto de Joshua J. Cotten en Unsplash

Estamos viviendo una explosión de la IA y la programación. Están entrando en juego numerosos actores y, entre ellos, no podía faltar Amazon para meter dentro de los servicios que provee un asistente para la programación que han llamado CodeWhisperer. En este artículo vamos a ver como podemos usarlo y que nos ofrece. Cabe destacar que está en período de vista previa y se espera que evolucione. Dentro de todo el ecosistema de productos, seguramente su rival más directo seria Github Copilot, que ya hemos visto en otros artículos.

0. Índice de contenidos.

  1. Introducción.
  2. Entorno.
  3. Creación del modelo.
  4. Creación del repositorio.
  5. Creación del servicio.
  6. Creación del controlador.
  7. Conclusiones.
  8. Referencias.

1. Introducción.

Amazon CodeWhisperer es una herramienta que nos ayuda a hacer pair programming basado en IA que genera sugerencias de código en tiempo real en nuestro IDE para ayudarnos a crear software.

CodeWhisperer Individual es de uso gratuito y solo necesitamos un inicio de sesión con AWS Builder.

La arquitectura de CodeWhisperer se basa en LLM (Large Language Model) entrenado con grandes cantidades de código.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Lenovo t480 (1.80 GHz intel i7-8550U, 32GB DDR4).
  • Sistema Operativo: Linux fedora 6.2.15-300.fc38.x86_64
  • IntelliJ IDEA 2023.1.2 (Ultimate Edition)

3. Creación del modelo.

Antes de comenzar, para usar CodeWhisperer se necesita:

El plug-in sería el siguiente:
aws toolkit plugin intellij

Una vez tenemos todo instalado siguiendo los pasos anteriores, veremos un panel donde podemos pausar o iniciar las sugerencias.

panel aws toolkit intellij

Al igual que pasaba con Github Copilot, CodeWhisperer no nos genera el fichero del proyecto. Vamos a seguir los mismos pasos que vimos en el tutorial de Github Copilot que es un proyecto generado con spring initializr.

Vamos a contar con las siguientes dependencias:

  • lombok
  • spring-boot-starter-data-jpa
  • h2
  • spring-boot-starter-web

Siendo todo parte de la última versión hasta la fecha de Spring Boot 3 (3.1.0)

El pom.xml queda asi:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>codewhisperer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>codewhisperer</name>
    <description>codewhisperer</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Al ser la primera entidad vamos a pedirle a CodeWhisperer que nos genere el código de la entidad.

Con // create a jpa entity class called Student with lombok annotations

Como podemos ver funciona como Github Copilot:

sugerencia code whisperer

Esa sugerencia es bastante comedida, nos pone mas sugerencias en comentarios que hace pensar que no sabe bien que es lombok. Tiene más sugerencias que nos da en una ventana como esta:

diferentes sugerencias code whisperer

Otro ejemplo de sugerencia sería este:

import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@NoArgsConstructor
@ALLArgsConstructor
@Builder
@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name
            = "first_name", nullable = false)
    private String firstName;
    @Column(name = "last_name", nullable = false)
    private String lastName;
    @Column(name = "email", nullable = false)
    private String email;
    @Column(name
            "phone", nullable = false)
    private String phone;
    @Column(name = "address", nullable = false)
    private String address;
    @Column(name = "city", nullable = false)
    private String city;
    @Column(name = "state", nullable = false)
    private String state;
    @Column(name = "zip", nullable = false)
    private String zip;

    @Column(name = "country", nullable = false)
    private String country;

    @Column(name = "dob", nullable = false)
    private String dob;

    @Column(name = "gender", nullable = false)
    private String gender;

    @Column(name = "nationality", nullable = false)
    private String nationality;

    @Column(name = "language", nullable = false)
    private String language;

    @Column(name = "ethnicity", nullable = false)
    private String ethnicity;

    @Column(name = "religion", nullable = false)
    private String religion;

    @Column(name = "school", nullable = false)
    private String school;

    @Column(name = "grade", nullable = false)
    private String grade;

    @Column(name = "major", nullable = false)
    private String major;

    @Column(name = "minor", nullable = false)
    private String minor;

    @Column(name = "gpa", nullable = false)
    private String gpa;

    @Column(name = "graduation_date", nullable = false)
    private String graduationDate;

    @Column(name = "resume", nullable = false)
    private String resume;

}

Aquí vemos que adolece del mismo problema que Github Copilot, que usa el API de persistencia de javax y no la de jakarta. Vamos a pedirle que nos genere el código con la API de Jakarta. Otro problema que tiene es que no deja que ningún campo sea nulo. Por último está usando palabras cortas como dob en lugar de dateOfBirth que sería más legible. Vamos a intentar que use jakarta

// jpa entity class called Student with lombok annotations using jakarta

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@NoArgsConstructor
@ALLArgsConstructor
@Builder
@Entity
@Table(name = "student")
public class Student {
    @Id
    @SequenceGenerator(
            name = "student_sequence",
            sequenceName = "student_sequence",
            allocationSize = 1
    )

    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "student_sequence"
    )

    private Long studentId;
    private String firstName;
    private String lastName;

    @Column(
            name = "email_address",
            nullable = false
    )

    private String emailId;

    @Embedded
    private Guardian guardian;

}

Ahora vemos que la respuesta no es consistente con la anterior, no ha generado los mismos campos, porque seguramente no tenga claro que es un estudiante y no itera sobre lo anterior como si fuera una conversación. De hecho es un poco raro poner a una columna emailId el nombre de email_address. Otra cosa rara es la clase Guardian que no existe. Tampoco ha usado las anotaciones de jakarta.

Si intentamos darle más pistas para indicar que estamos usando spring boot 3 con el siguiente prompt:

// create a jpa entity class called Student with lombok annotations an id and a name for springboot 3

Nos genera lo siguiente:

public class Student {
    private Long id;
    private String name;

    public Student() {

    }

    public Student(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

}

Nos damos cuenta que ha omitido toda la parte de lombok y no ha puesto nada de spring. Parece que le hemos confundido mas que ayudado. En algunas ocasiones puede parecer que mas que ayudarnos a nosotros, somos nosotros los que tenemos que ayudarle a él.

El mejor resultado para esta entidad lo hemos conseguido con el siguiente prompt:

// create a jpa entity class called Student with lombok annotations

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

}

Es la clase más comedida en cuanto a campos y anotaciones. Con unos pocos retoques quedaría como nosotros queremos.

Da la sensación que a medida que hemos ido avanzando nos ha ido conociendo algo más y va afinando resultados.

También, a medida que vamos usando este tipo de herramientas nos vamos dando cuenta que estos asistentes están para asistir y que puede que intentar que nos genere entidades desde cero, sea mucho.

Si queremos que nos vaya ayudando en lugar de ponerle un comentario, se empeña en crearnos un User y no un Student.

Sugerencia user

Lo dejamos así.

package com.example.codewhisperer.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;


}

4. Creación del repositorio.

Para el repositorio, hemos intentado que nos asista, pero las sugerencias que nos da no son muy buenas. En el StudentRepository nos sugiere que usemos la entidad User.

sugerencia 1 repositorio
sugerencia 2 repositorio

import com.example.codewhisperer.model.User;

Ayudándole se puede conseguir que genere el repositorio. Si cambiamos la entidad de dominio por la nuestra mejora, pero por ejemplo no le pone los tipos al JpaRepository que sería algo así como JpaRepository<Student, Long>. Es como si no tuviera contexto y por eso nos sugiere primero poner un User y luego no nos da la sugerencia de usar Student y Long en el JpaRepository.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.codewhisperer.model.Student;

@Repository
public interface StudentRepository extends JpaRepository {

}

Con la interfaz vacía, nos sugiere métodos que no tienen mucho sentido, ya que por ejemplo nuestra entidad no tiene un campo email. Lo mismo pasa con otros campos, el único campo que tiene la entidad es el id.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.codewhisperer.model.Student;

@Repository
public interface StudentRepository extends JpaRepository {
    Student findByEmail(String email);

    Student findByEmailAndPassword(String email, String password);

    Student findById(long id);

    Student findByEmailAndPasswordAndId(String email, String password, long id);

    Student findByEmailAndPasswordAndIdAndDeleted(String email, String password, long id, boolean deleted);

    Student findByEmailAndPasswordAndDeleted(String email, String password, boolean deleted);

    Student findByEmailAndDeleted(String email, boolean deleted);

    Student findByEmailAndIdAndDeleted(String email, long id, boolean deleted);

    Student findByEmailAndId(String email, long id);

    Student findByEmailAndDeletedAndId(String email, boolean deleted, long id);

    Student findByEmailAndDeletedAndIdAndPassword(String email, boolean deleted, long id, String password);

    Student findByEmailAndDeletedAndIdAndPasswordAndDeleted(String email, boolean deleted, long id, String password, boolean deleted2);
}

Lo dejamos así.

package com.example.codewhisperer.repository;
import com.example.codewhisperer.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

}

5. Creación del servicio.

Para el servicio, pasa lo mismo que con el repositorio, empieza a sugerirnos el uso de List y Optional, que son muy comunes. Pero la tercera sugerencia es que usemos la entidad User que no existe.

import java.util.List;
import java.util.Optional;

import com.example.codewhisperer.model.User;

Al igual que con el repositorio, si le ayudamos un poco, vamos a conseguir que nos genere el servicio. Le ponemos el modelo correcto y nos genera el servicio, pero como antes, nos empieza a sugerir métodos que no pueden darse, ya que no existen en la entidad.

import java.util.List;
import java.util.Optional;

import com.example.codewhisperer.model.Student;

public interface StudentService {

    List<Student> findAll();

    Optional<Student> findById(Long id);

    Student save(Student student);

    void deleteById(Long id);

    void deleteAll();

    List<Student> findByLastName(String lastName);

    List<Student> findByFirstName(String firstName);

    List<Student> findByLastNameAndFirstName(String lastName, String firstName);

    List<Student> findByLastNameOrFirstName(String lastName, String firstName);

    List<Student> findByLastNameAndFirstNameAndAge(String lastName, String firstName, Integer age);

    List<Student> findByLastNameAndFirstNameAndAgeOrAge(String lastName, String firstName, Integer age, Integer age2);

    List<Student> findByLastNameAndFirstNameAndAgeAndAgeOrAge(String lastName, String firstName, Integer age, Integer age2, Integer age3);

    List<Student> findByLastNameAndFirstNameAndAgeAndAgeOrAgeOrAge(String lastName, String firstName, Integer age, Integer age2, Integer age3, Integer age4);
}

Lo dejamos así.

package com.example.codewhisperer.service;

import com.example.codewhisperer.model.Student;

import java.util.List;

public interface StudentService {
    List<Student> getAllStudents();
    Student getStudentById(Long id);
    Student createStudent(Student student);
    Student updateStudent(Student student);
    void deleteStudent(Long id);
}

Vamos con la implementación.

import com.example.codewhisperer.model.Student;
import org.springframework.stereotype.Service;

@Service
public class StudentServiceImpl {

    public Student getStudent(String id) {
        return new Student(id, "John", "Doe");
    }

    public Student getStudent(String id, String firstName, String lastName) {
        return new Student(id, firstName, lastName);
    }

    public Student getStudent(String id, String firstName, String lastName, String email) {
        return new Student(id, firstName, lastName, email);
    }

    public Student getStudent(String id, String firstName, String lastName, String email, String phone) {
        return new Student(id, firstName, lastName, email, phone);
    }

    public Student getStudent(String id, String firstName, String lastName, String email, String phone, String address) {
        return new Student(id, firstName, lastName, email, phone, address);
    }

    public Student getStudent(String id, String firstName, String lastName, String email, String phone, String address, String city) {
        return new Student(id, firstName, lastName, email, phone, address, city);
    }
}

Dándole otro pequeño empujón, le indicamos que es lo que queremos (que nos implemente el StudentService) y ya si nos va ofreciendo más ayuda. Aun así, necesitamos dejarle casi todo hecho.

import com.example.codewhisperer.model.Student;
import com.example.codewhisperer.model.repository.StudentRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class StudentServiceImpl implements StudentService {

    private final StudentRepository studentRepository;

    public StudentServiceImpl(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @Override
    public List<Student> findAll() {
        return studentRepository.findAll();
    }

    @Override
    public Optional<Student> findById(Long id) {
        return studentRepository.findById(id);

    }

    @Override
    public Student save(Student student) {
        return studentRepository.save(student);
    }

    @Override
    public void deleteById(Long id) {
        studentRepository.deleteById(id);
    }

    @Override
    public void deleteAll() {
        studentRepository.deleteAll();
    }
}

Como vemos a continuación, muchas de las sugerencias tienen código de más o nos da tests un poco dudosos. Hay que ir jugando y tener paciencia con las recomendaciones.

sugerencia 1 servicio
sugerencia 1 servicio
sugerencia 2 servicio
sugerencia 2 servicio

Queda así

package com.example.codewhisperer.service;

import com.example.codewhisperer.model.Student;
import com.example.codewhisperer.repository.StudentRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {

    private StudentRepository studentRepository;

    public StudentServiceImpl(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @Override
    public List<Student> getAllStudents() {
        return studentRepository.findAll();
    }

    @Override
    public Student getStudentById(Long id) {
        return studentRepository.findById(id).orElseThrow(() -> new RuntimeException("Student not found"));
    }

    @Override
    public Student createStudent(Student student) {
        return studentRepository.save(student);
    }

    @Override
    public Student updateStudent(Student student) {
        return studentRepository.save(student);
    }

    @Override
    public void deleteStudent(Long id) {
        studentRepository.deleteById(id);
    }
}

6. Creación del controlador.

En el caso del controlador, nos ayuda más. El path que nos sugiere esta bastante bien y a medida que vamos implementando la clase, nos va sugiriendo métodos. Da la impresión que los controladores los genera mejor, tal vez porque son más sencillos y sus implementaciones suelen ser siempre más o menos iguales. En cambio en servicios y repositorios se puede meter lógica de negocio.

import com.example.codewhisperer.service.StudentService;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @GetMapping
    public String getStudents() {
        return studentService.getStudents();
    }
}

Pero tenemos que ir corrigiendo los tipos que devuelve. También vemos que se lía con el nombre de los métodos. Lo dejamos solo con el get

package com.example.codewhisperer.controller;

import com.example.codewhisperer.model.Student;
import com.example.codewhisperer.service.StudentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/v1/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @GetMapping
    public List<Student> getStudents() {
        return studentService.getAllStudents();
    }
}

A medida que hemos ido implementando cosas, ha ido metiendo endpoints que apuntan a tests. Parece que está mezclando la implementación con los tests, pero no ha sugerido importar nada relacionado con los tests.

@GetMapping("/test")
    public String test() {
        return "test";

    }

7. Conclusiones.

Todos los ejemplos que hemos visto, al igual que anteriores artículos, no son deterministas, por lo que siguiendo los mismos pasos, se pueden llegar a situaciones distintas.

Basándonos en lo que hemos probado de Amazon CodeWhisperer, creemos que tiene potencial, pero mucho camino por recorrer. Parece que está un paso por detrás de Github Copilot, pero estando Amazon detrás, seguro que consigue ponerse a la altura.

Tras las pruebas que se han hecho con CodeWhisperer y Copilot, da la sensación que no son muy buenos de momento a la hora de crear cosas desde cero, o que cuando hay muchas posibilidades, no dan muy buenos resultados. Por ejemplo, un controlador siempre suele ser una llamada al servicio. En cambio, con los servicios, donde seguramente ha visto código con más lógica, no ha sido muy útil.

Los ejemplos que siempre nos ponen, son ejemplos de algoritmos, donde ellos tienen ventaja, ya que si es un problema matemático conocido, es más fácil que ellos sepan la solución óptima, mientras que nosotros podemos no ser conscientes de que hay varias alternativas o incluso no saber que ese problema tiene una solución conocida y hacer nosotros una solución que no sea la más óptima o legible.

Si vemos la solución de geeksforgeeks:

// Java program for implementation of QuickSort
class QuickSort
{
	/* This function takes last element as pivot,
	places the pivot element at its correct
	position in sorted array, and places all
	smaller (smaller than pivot) to left of
	pivot and all greater elements to right
	of pivot */
	int partition(int arr[], int low, int high)
	{
		int pivot = arr[high];
		int i = (low-1); // index of smaller element
		for (int j=low; j<high; j++)
		{
			// If current element is smaller than or
			// equal to pivot
			if (arr[j] <= pivot)
			{
				i++;

				// swap arr[i] and arr[j]
				int temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}

		// swap arr[i+1] and arr[high] (or pivot)
		int temp = arr[i+1];
		arr[i+1] = arr[high];
		arr[high] = temp;

		return i+1;
	}


	/* The main function that implements QuickSort()
	arr[] --> Array to be sorted,
	low --> Starting index,
	high --> Ending index */
	void sort(int arr[], int low, int high)
	{
		if (low < high)
		{
			/* pi is partitioning index, arr[pi] is
			now at right place */
			int pi = partition(arr, low, high);

			// Recursively sort elements before
			// partition and after partition
			sort(arr, low, pi-1);
			sort(arr, pi+1, high);
		}
	}

	/* A utility function to print array of size n */
	static void printArray(int arr[])
	{
		int n = arr.length;
		for (int i=0; i<n; ++i)
			System.out.print(arr[i]+" ");
		System.out.println();
	}

	// Driver program
	public static void main(String args[])
	{
		int arr[] = {10, 7, 8, 9, 1, 5};
		int n = arr.length;

		QuickSort ob = new QuickSort();
		ob.sort(arr, 0, n-1);

		System.out.println("sorted array");
		printArray(arr);
	}
}
/*This code is contributed by Rajat Mishra */

La respuesta de codewhisperer es más rebuscada. Si optamos por la versión estática con clase, acabamos en un callejón sin salida

Quicksort estático
Quicksort estático
Implementación quicksort estático
Implementación quicksort estático

Si optamos por la versión estática de la función sin clase (la primera sugerencia que nos da), este es el código

public static void main(String[] args) {
        final int[] arr = {5, 9, 4, 6, 5, 3};
        // Sort the array using quick sort
        quickSort(arr, 0, arr.length - 1);
        // Print the sorted array
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }

    private static void quickSort(int[] arr, int i, int i1) {
        int pivot = arr[i];
        int low = i;
        int high = i1;

        while (low < high) {
            while (arr[high] >= pivot && low < high) {
                high--;
            }
            while (arr[low] <= pivot && low < high) {
                low++;
            }
            if (low < high) {
                int temp = arr[low];
                arr[low] = arr[high];
                arr[high] = temp;
            }
        }
        arr[i] = arr[low];
        arr[low] = pivot;
        if (i < low - 1) {
            quickSort(arr, i, low - 1);
        }
        if (i1 > low + 1) {
            quickSort(arr, low + 1, i1);
        }
        System.out.println(arr);
        System.out.println(low);
        System.out.println(high);
        System.out.println(i);
        System.out.println(i1);
    }

Hemos tenido que crear la clase y la función desde el IDE porque no nos da una respuesta entera.

Puede que cuando esto avance, en vez de pasarnos tiempo pensando en soluciones, pasemos tiempo pensando en mejores soluciones y haciendo software de mayor calidad o más óptimo.

Esto no ha hecho más que empezar, hay muchas empresas con muchos intereses que están apostando fuerte en estas tecnologías, así que veremos un avance exponencial y, donde ahora vemos que le tenemos que ayudar más, nos ayudara más él a nosotros.

También hay que tener cuidado con la privacidad de estas herramientas en general.

8. Referencias.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad