Monday, July 7, 2025

Un Agente simple para realizar resumen del contenido de sitios web con Embabel

La AI (Artificial Intelligence) esta por todos lad

La AI (Artificial Intelligence) esta por todos lados y ha llego para quedarse.

Un gran uso de la AI para nosotros como Desarrolladores de Software es la creaci贸n de Agentes Inteligentes que, con ayuda de Large Language Models (LLM’s), puedan resolver problemas que ser铆an complejos o imposibles de abordar mediante programaci贸n tradicional.

Hace unas semanas me entere de la existencia de un nuevo framework en el que Rod Johnson (creador del framework Spring) y otras personas est谩n trabajando llamado Embabel

Embabel es un framework para crear flujos de agentes en la JVM haciendo una mezcla de interacciones con LLM’s via prompts y c贸digo con modelos de dominio (clases Java/Kotlin). El framework esta construido sobre Spring AI

El framework es relativamente nuevo y aun en desarrollo, aun no existe documentaci贸n oficial y es posible que algunas cosas que aqu铆 explico cambien en un futuro (aunque no creo que radicalmente).

El c贸digo de este ejemplo lo encuentran en mi repositorio de Github en la direcci贸n: abadongutierrez/basic-embabel-agent

Caso de Uso: Resumen de sitios web

Casi todos hemos usado LLM’s para realizar alg煤n resumen de alg煤n texto, de hecho, hacer resumen es uno de los grandes usos de LLM’s, y en el ejemplo de hoy usaremos Embabel para crear un Agente que extraiga el contenido de los sitios que le digamos y que haga un resumen del texto de los mismos.

En general usaremos Embabel para construir un agente que: 1. Reciba una entrada de texto libre por parte del usuario (via Spring Shell). 2. Extraiga los enlaces web mencionados por el usuario. 3. Visite cada sitio, obtenga su contenido en forma de texto libre de etiquetas HTML. 4. Genere un resumen del contenido de cada sitio.

Para visitar cada liga y extraer el contenido de ese sitio web usaremos la biblioteca JSoup. Con esta biblioteca podemos f谩cilmente conectarnos a un sitio web y extraer solo el texto sin etiquetas HTML de la siguiente forma:

// Conectarse y obtener el documento HTML
Document doc = Jsoup.connect("https://en.wikipedia.org/").get();
// Extraer solo el texto
doc.text();

¿C贸mo se crea un Agente?

Para definir un Agente tenemos que crear una clase y anotarla con @Agent. Esto es muy similar al uso @Component y las anotaciones derivadas que existen en el Framework de Spring. De hecho @Agent tambi茅n deriva de @Component por lo que se maneja como un Bean y, por lo mismo, podemos aprovechar la inyecci贸n de dependencias.

@Agent(description = "Agent to summarize content of web pages")  
public class SummarizingAgent {
    @Action  
    public WebPageLinks extractWebPagesLinks(UserInput userInput) { ... }

    @Action
    public SummarizedPages summarizeWebPages(WebPageLinks webPageLinks, OperationContext operationContext) { ... }

    @AchievesGoal(description = "Show summarized content of the web pages to the user")  
    @Action  
    public SummarizedPages showSummarization(SummarizedPages summarizedPages) { ... }
}

Es importante asignar una buena descripci贸n al agente, ya que cuando se interact煤a con ellos a trav茅s de Spring Shell, un LLM es el encargado de seleccionar qu茅 agente responder谩 a la solicitud del usuario. Esta selecci贸n se basa en un an谩lisis de la intenci贸n del usuario y en la correspondencia con el agente m谩s adecuado para atenderla.

Cada m茅todo que represente un paso en el flujo del agente debe anotarse con @Action. El m茅todo que representa el objetivo final del agente tambi茅n se anota con @AchievesGoal.

Cuando interactuamos con Agentes via la interfaz de Spring Shell, generalmente el primer paso es el m茅todo @Action que recibe como argumento un UserInput. Esto lo menciono porque usar los Agentes via Spring Shell no es la 煤nica forma de interactuar con ellos tambi茅n se puede con otros mecanismos que tratare de explorar en futuras publicaciones.

El flujo del Agente

No existe una forma de especificar el flujo del Agente program谩ticamente. El framework, como lo dice en la pagina de inicio, trata de ir mas all谩 de simplemente especificar un flujo a trav茅s de una maquina de estados y aplica una planeaci贸n inteligente al inicio del flujo y despu茅s de la ejecuci贸n de cada paso. El flujo lo detecta el framework a trav茅s de la relaci贸n que hay entre los m茅todos inspeccionando los tipos de datos en de “entradas” (argumentos de los m茅todos) y “salidas” (el tipo de retorno).

SummarizingAgent

En este tutorial creamos el SummarizingAgent que como primer paso extrae las URLs de la entrada de usuario. Para lograr esto hacemos uso de un LLM debido a que como las instrucciones del usuario es texto libre y sin formato, los LLM son buenos analizando texto y extrayendo informaci贸n que nosotros le indiquemos. Esto esta implementado en el m茅todo extractWebPagesLinks.

@Action  
public WebPageLinks extractWebPagesLinks(UserInput userInput) {  
    String prompt = String.format("""  
            Extracts the urls from the provided user input.

            <user-input>  
            %s
            </user-input>

            Extract only the links mentioned in the user input, dont add any other links.  
            """.trim(), userInput.getContent());  
    return PromptRunner.usingLlm().createObjectIfPossible(prompt, WebPageLinks.class);  
}

El segundo paso en el flujo es extraer el contenido en texto de cada sitio web y aqu铆 nos apoyarnos nuevamente de un LLM para obtener un resumen de ese contenido. Esto esta implementado en el m茅todo summarizeWebPages. Este m茅todo tiene 2 formas de actuar y esto depende de la bandera app.useOpenAI definida en el application.properties. Esta peque帽a aplicaci贸n esta pensada para usar Ollama y los modelos locales llama3.2 y all-minilm pero tambi茅n se puede hacer uso de OpenAI asignando la bandera app.useOpenAI en true, esto implica que se necesita especificar la variable de ambiente OPENAI_API_KEY para que la aplicaci贸n funcione correctamente.

El m茅todo summarizeWebPages lo implemente de 2 formas debido a que llama3.2 no es un modelo tan poderoso como los modelos de OpenAI y tuve muchas complicaciones usando el mismo prompt. Por lo que para cuando se usa llama3.2 use un prompt distinto y ademas una alternativa en caso de que el primer prompt fallara.

...
@Value("${app.useOpenAI:false}") boolean useOpenAI

...

@Action  
public SummarizedPages summarizeWebPages(WebPageLinks webPageLinks, OperationContext operationContext) {  
    if (this.useOpenAI) {  
        return getSummarizedPagesUsingOpenAI(webPageLinks);  
    }  
    return getSummarizedPagesUsingLocalModels(webPageLinks, operationContext);  
}

El ultimo paso en el flujo es simplemente devolver el conjunta de paginas y su resumen para que el framework lo imprima en la consola de Spring Shell. Esto implementado en el m茅todo showSummarization que ademas tiene que estar marcado con la anotaci贸n @AchievesGoal debido a que una vez ejecutando este m茅todo el objetivo del agente se habra logrado.

@AchievesGoal(description = "Show summarized content of the web pages to the user")  
@Action  
public SummarizedPages showSummarization(SummarizedPages summarizedPages) {  
    return summarizedPages;  
}

Interacci贸n con LLMs

El framework tiene el concepto de PromptRunner‘s que como su nombre lo indica ejecutan un prompt a un LLM.

Un PromptRunner tiene m茅todos para ejecutar un prompt y convertir la salida del prompt a un objeto de dominio con m茅todos como createObjectIfPossible o createObject. Esto da la ventaja de aplicar tipado fuerte en nuestros programas y as铆 poder hacer uso de t茅cnicas de refactoring mas f谩cilmente.

El framework define ciertos LLM que los programas utilizaran por default. En nuestro caso estamos usando modelos locales con Ollama por lo que en el archivo application.properties podemos encontrar las siguientes propiedades que indican que modelos se estar谩n usando por default:

embabel.models.default-llm=llama3.2:latest  
embabel.models.default-embedding-model=all-minilm:latest  
embabel.models.embedding-services.best=all-minilm:latest  
embabel.models.embedding-services.cheapest=all-minilm:latest  
embabel.models.llms.best=llama3.2:latest  
embabel.models.llms.cheapest=llama3.2:latest  

embabel.agent-platform.ranking.llm=llama3.2:latest

Cuando se realizan operaciones con LLM una de las principales cosas que se deben especificar son los prompts pero los PromptRunner‘s tambi茅n dan la facilidad de especificar los “Tools” que deseamos utilizar como parte de la ejecuci贸n de un prompt. En esta peque帽a aplicaci贸n estamos especificando y haciendo use de JSoup como “Tool” para extraer el texto de un sitio web.

Las “Tools” se pueden especificar haciendo uso de la anotaci贸n de Spring AI @Tool y esto se puede ver implemetado en la clase JSoupTool que ademas es un Bean de spring que f谩cilmente inyectamos al Agente.

@Component  
public class JSoupTool {
    ...

    @Tool(name = "jsoup", description = "A tool to extract text from web pages using JSoup")  
    public String getPageTextTool(String url) {  
        ...
    }

Los PromptRunner‘s hacen uso de los LLM especificados por default o usando LlmOptions se puede indicar modelos distintos. En el caso de esta aplicaci贸n usamos esta funcionalidad para especificar el modelo de OpenAI a utilizar cuando la bandera app.useOpenAI esta activa.

String prompt = " ... ";
BuildableLlmOptions llmOptions = LlmOptions.fromCriteria(  
        ModelSelectionCriteria.byName("gpt-4.1-mini")  
);  
return PromptRunner  
        .usingLlm(llmOptions)  
        .withToolObject(jSoupTool)  
        .createObjectIfPossible(prompt, SummarizedPages.class);

Ejecuci贸n

Esta aplicaci贸n esta configurada para correr usando como interfaz Spring Shell y eso lo podemos notar por la anotaci贸n @EnableAgentShell en la clase donde se encuentra el m茅todo main.

@SpringBootApplication  
@EnableAgents(  
       loggingTheme = LoggingThemes.STAR_WARS,  
       localModels = {LocalModels.OLLAMA}  
)  
@EnableAgentShell
public class BasicEmbabelAgentApplication {  
    public static void main(String[] args) {  
       SpringApplication.run(BasicEmbabelAgentApplication.class, args);  
    }  
}

Tambi茅n como se puede notar esta configurada para buscar y usar LLM’s locales usando Ollama.

Una vez ejecutada la aplicaci贸n aparece el prompt de Spring Shell donde podemos usar el comando x (execute) para indicar el “user input” y hacer que la plataforma de agentes de Embabel busque y seleccione el agente adecuado para atender la petici贸n del usuario:

...
21:05:55.957 [main] INFO  DelegatingAgentScanningBeanPostProcessor - All deferred beans were post-processed.
21:05:55.958 [main] INFO  BasicEmbabelAgentApplication - Started BasicEmbabelAgentApplication in 1.834 seconds (process running for 2.074)
Fear is the path to the dark side.
starwars> x "summarize the content of the following page https://en.wikipedia.org/wiki/Alan_Turing"

salida:

You asked: UserInput(content=summarize the content of the following page https://en.wikipedia.org/wiki/Alan_Turing, timestamp=2025-07-08T03:15:04.089557Z)

{
  "summarizedPages" : [ {
    "url" : "https://en.wikipedia.org/wiki/Alan_Turing",
    "summary" : "Alan Turing (1912-1954) was a British mathematician, computer scientist, logician, philosopher, and cryptographer who made significant contributions to the development of computer science, artificial intelligence, and cryptography.\n\n**Early Life and Education**\n\nTuring was born on June 23, 1912, in London, England. He studied mathematics at King's College, Cambridge, where he graduated with a First Class Honours degree in Mathematics. During World War II, Turing worked at the Government Code and Cypher School (GC&CS) at Bletchley Park, where he played a crucial role in cracking the German Enigma code.\n\n**Contributions to Computing**\n\nTuring is considered one of the founders of computer science. He proposed the theoretical foundations of modern computer science, including:\n\n1. **The Turing Machine**: a mathematical model for a computer's central processing unit (CPU).\n2. **The Universal Turing Machine**: a machine that could simulate any other machine.\n3. **Computability Theory**: the study of what can be computed by a machine.\n\n**Codebreaking and Cryptography**\n\nAt Bletchley Park, Turing worked with a team to crack the Enigma code, which was used by the German military during World War II. His work significantly contributed to the Allied victory.\n\n**Personal Life and Later Years**\n\nTuring's personal life was marked by tragedy. In 1952, he was convicted of gross indecency for his relationship with a man, which led to his chemical castration and eventual death in 1954 at the age of 41.\n\n**Legacy**\n\nTuring's legacy is profound:\n\n1. **Computer Science**: Turing's work laid the foundation for modern computer science.\n2. **Artificial Intelligence**: His ideas on machine intelligence and computation have influenced AI research.\n3. **Cryptography**: Turing's contributions to codebreaking and cryptography have had a lasting impact on national security.\n\n**Recognition**\n\nIn 2009, the British government officially apologized for Turing's treatment and posthumously pardoned him. In 2017, he was featured on the £50 note, making him the first openly gay person to be featured on a British banknote.\n\nTuring's life and work serve as a testament to his innovative spirit and contributions to science and society. His legacy continues to inspire new generations of computer scientists, mathematicians, and thinkers."
  } ]
}

Si analizan con atenci贸n los “logs” que imprime la aplicaci贸n al ejecutar notaran los pasos que toma Embabel para seleccionar el Agente a ejecutar, los action/goals que contiene el Agente y la planeacion que hace de la ejecuci贸n de los “actions” despu茅s de la ejecuci贸n de cada paso.

Conclusion

Aunque Embabel a煤n se encuentra en una etapa de desarrollo, ya demuestra ser una propuesta prometedora para los desarrolladores que trabajamos sobre la JVM. Embabel esta desarrollado en Kotlin pero, Rod Johnson lo ha mencionado en entrevistas, este debe poder usarse de manera natural en Java como se puede ver en el c贸digo de este ejemplo.

Su enfoque declarativo permite crear agentes inteligentes usando anotaciones, sin definir flujos de forma expl铆cita. En su lugar, un algoritmo de IA (sin usar LLMs) infiere el plan de ejecuci贸n seg煤n el contexto del agente y despu茅s de ejecutar cada paso. Adem谩s, se integra de forma nativa con tecnolog铆as conocidas como Spring y Spring AI, lo que facilita su adopci贸n. Tambi茅n incluye soporte para pruebas unitarias y de integraci贸n, lo que lo hace apto para proyectos serios desde el inicio.

Wednesday, May 8, 2024

The new Java main()

`public static void main(String[] args)` is (or wa

public static void main(String[] args) is (or was) the infamous entry point in all Java programs

class MyApp1 {
	public static void main(String[] args) {
		System.out.println("Hello World!");
	}
}
# Using Java 21

$ javac MyApp1.java
$ java MyApp1
Hello World!

Now, since Java 21 the Java Team added the instance main methods feature that allows Java Programs to be launched with simple instance methods named main that no need to be static, nor public (but they cannot be private for obvious reasons), nor receive parameters:

class MyApp2 {
	void main() {
		System.out.println("Hello World!");
	}
}
# Using Java 21

$ javac MyApp2.java
$ java --enable-preview MyApp2
Hello World!

then also they added another feature called unnamed classes that add the ability to declare classes in an implicit manner. Lets say we create a file called MyApp3.java and then in that file we write:

void main() {
	System.out.println("Hello World!");
}

this main method is an instance method in a implicit class named MyApp3 (the class its taking its name from the file name).

# Using Java 21

$ javac --enable-preview --release 21 MyApp3.java
$ java --enable-preview MyApp3
Hello World!

and soon in Java 23 every implicit class will automatically import static methods from package java.io.IO.* which will allow to write the main method much simpler.

void main() {
	println("Hello World!");
}

but again this feature is not yet released not even as a preview.

Saturday, October 28, 2023

A Sip of Java: Maps from collections of objects using Collectors (groupingBy and partitioningBy)

 We can use streams and collectors to get Maps from a collection of objects. These Maps could be the result of our collection grouped or partitioned in specific ways.

Let's say we have a collection of Employees, where an Employee has attributes like: name, years of experience, tenure (in the company), role name, experience level.

Now let's say we want to group our employees by the experience level:

What if we dont need to know the employees with some experience level but the total number of employees with that experience level?:

In this case we are using the version of the method Collectors.groupingBy that uses another Collector as downstream collector.

Ok, what if then, we have to partition the collection based on some condition that could be true or false? In that case we use Collectors.partitioningBy:

Collectors.partitioningBy method also has a version that receives another Collector as downstream collector where we can execute other interesting logic, like grouping the result.

Wednesday, November 30, 2022

A Sip of Java: Encapsulate collection attributes

Encapsulation is the ability we give our classes (and objects created from them) to hide the implementation details of their inner workings and control the consistency of the internal state and invariants.

It happens all the time that we expose some attribute in our class to the outside world in the form of a collection like the following:

This is actually pretty common and I think a bad practice, we tend to create private fields in our classes and as a reflex create setters and getters immediately without thinking about the consequences of exposing the state that way.

If we simply return the reference to the attribute of the object there is a lot of danger there because now others have a copy of the reference to that attribute that is supposed to be private and under the control of the owner object.

Imagine if some code that gets that list of emails from our class now tries to modify the content of that list by adding or removing some objects from it. How can we control that? what if there is some kind of limit on the number of emails a Contact can have? How can we ensure only valid emails are added?

What if, instead, we write the following:

Now users of our class will get a list of emails that cannot be modified. New Email objects cannot be added or removed (but still the objects in the list can be changed if the public methods in the Email class allow that 馃槈). You could also create a copy of the list of emails by instantiating a new ArrayList sending the list of emails as an argument to the constructor new ArrayList<>(this.emails) but I still think it is better to fail when trying to modify the list.

Probably it is even better to just expose public methods with just the exact functionality that we want to allow like the following:

With the last implementation, we are hiding more and then have even more control. For example, let's say a Contact can have thousands of Emails (I know, it is unlikely, but let's say that is possible), then we can even change internally the way we store Emails from a List to a Map and then be able to have better performance for some operations, like removing an Email.

Do you have suggestions on how to improve the information hiding here? please comment!

Wednesday, November 23, 2022

A Sip of Java: Stream.flatMap()

 Let's say you have a map where you store a list of values for each key, something like:


And now you want a single list with all values in the Map. Then you need to use flatMap:

Map.values() returns a collection of the values for each key in the map, since each value is a list of Strings, it will return a collection of list of strings.

But you want a single list with all strings right?

Ok, then use Stream.flatMap() method to get a stream for each list in the collection of values and then merge all lists into a single list.

Simply put, convert a Stream of Stream of values into a single list of values.

Monday, August 15, 2016

Desde el 2012 no he escrito nada en este blog. Deberia retomarlo.

Tuesday, October 2, 2012

Linux: Administraci贸n de espacio en disco


Todos nos hemos topado con el problema de espacio en disco, ya sea porque en nuestra maquina se nos acaba el espacio por tanto mp3's o videos o en el servidor que estamos administrando los backups y logs que se generan empiezan a consumir ese precioso espacio que al final nos puede afectar en el rendimiento del sistema operativo y por consiguiente de las aplicaciones que ah铆 corren. Es necesario conocer los comandos adecuados para conocer el espacio que se esta ocupando, quien lo esta ocupando y ver que podemos eliminar para poder tener mas.

Conocer el espacio usado en disco

Lo que primero hay que conocer es el total de espacio que estamos usando en el disco duro y para realizar eso usamos el comando:

df -h

El cual nos dice el espacio del Sistema de archivos en la columna Size, el espacio usado en la columna Used, el espacio disponible en la columna Avail y el porcentaje que representa el espacio usado  y por ultimo el punto de montaje de ese sistema de archivos. Se usa la opci贸n "h" (human-readable) para que nos de valores que nos arroje el comando vengan en valores de Kilobytes, Megabytes o Gigabytes y no en bloques que es como por default imprime los valores.

Conocer el espacio usado por un folder en especifico
Ahora, si lo que deseamos conocer es el espacio especifico de un folder debemos correr el comando:

du folder -ha

Donde folder es el nombre o ruta hacia el folder del cual queremos conocer el espacio que esta usando. Como salida este comando imprime el tama帽o de cada uno de los archivos y folders contenidos dentro de el. Se usa la opci贸n "h" para, igual que en el comando 'df', los valores que arroje vengan en Kilobytes, Megabytes o Gigabytes. La opci贸n "a" (all) se usa para que se impriman el tama帽o tanto folders como archivos ya que si no se usa esta opci贸n solo se imprimen folders.

Existen otras opciones a usar para este comando y como en la mayor铆a de los comandos en linux esas opciones se puede conocer ejecutando el comando de la siguiente forma:


du --help

La salida del comando 'du' se podr铆a ordenar para as铆 poder obtener, por ejemplo, los 10 folders y archivos que son los que ocupan mas espacio dentro de un cierto folder:


du folder -ka | sort -n -r | head -n10

Se usa la opcion 'k' en el comando 'du' para que en lugar de que nos imprima valores en Kilobytes, Megabytes o Gigabytes solo imprima todos los valores unificados a Kilobytes y el comando 'sort' pueda ordenarlos de manera uniforme. Y para solo obtener los 10 primeros se utiliza el comando 'head'. As铆 que si, por ejemplo, se desea obtener los primeros 5 solo hay que variar el valor en la opci贸n 'n' del comando 'head'.

Pero si lo que se desea saber es el tama帽o total de cierto folder sin tener que ver el tama帽o de su contenido se usa el comando:


du folder -hs

La opci贸n 's' (summarize) hace que solo se imprima el valor total del folder si que se muestre el tama帽o de cada folder y archivo que contiene.

Hay que tomar en cuenta que para obtener los valores de tama帽o el comando tiene que recorrer el contenido del folder para ir sumando el tama帽o de cada elemento que contiene, as铆 que, si el folder es muy grande la ejecuci贸n de este comando puede tardar.

Borrar archivos y folders

Ya que se conoce el elemento que se desea borrar se hace uso del comando 'rm' para eliminarlo. Si se trata de un archivo se usa el comando:

rm archivo

pero si se trata de un folder y nos interesa borrar el mismo y su contenido, se usa el comando:

rm -rf folder

hay que tomar en cuenta que una ves borrados ya no es posible recuperarlos as铆 que estos comandos se tienen que usar con mucho cuidado.

Tuesday, August 7, 2012

Archivo basico log4j.properties

El archivo simple de lo4j.properties con el que inicio en los proyectos y como siempre se me esta olvidando, mejor lo posteo:

Noten que esta el appender de Spring Framework, si no lo usan pues quitenlo.