En sólo un par de años, la Inteligencia Artificial ha pasado de ser una tendencia emergente a un componente esencial del software moderno. ChatGPT, Grok, Gemini y otros asistentes de IA juegan ahora un papel importante en la vida diaria de todos, tanto profesional como personalmente.
Es por eso que 4D 21 introduce 4D.Vectors y 4D AI Kit: para dar a los desarrolladores 4D herramientas simples y eficaces para añadir funcionalidades potenciadas por IA a sus aplicaciones.
Ya hemos compartido muchos ejemplos, tutoriales y webinars sobre IA, pero hace poco me pregunté: ¿qué haría falta para introducir IA en una aplicación 4D de hace 30 años?
¿Podría simplemente preguntar a dicha aplicación por sus 10 principales clientes y obtener al instante un bonito gráfico de vuelta?
Bueno, ¿adivinen qué? Resultó ser tan sencillo que merece su propia entrada en el blog.
Vamos a empezar con el enlace de las fuentes del proyecto en Github y un video de demostración rápida
La aplicación 4D Invoice
La aplicación 4D Invoice ha estado disponible en 4D Depot durante bastante tiempo. Sirve como un ejemplo sólido de cómo gestionar productos, clientes y facturas de una manera limpia y estructurada. Aunque ya utiliza el modo proyecto (un requisito para aprovechar las últimas funcionalidades de 4D AI) todavía no utiliza DataClasses, y sus formularios son anteriores a la más reciente clase Form. El código base es bastante grande, utiliza patrones de diseño genéricos e incluye varias reglas de negocio no triviales.
Para este experimento, mi objetivo era añadir un nuevo formulario que permitiera la interacción con la aplicación mediante IA. Quería que los usuarios pudieran conversar con un asistente de IA y formular desde preguntas sencillas hasta consultas analíticas más complejas. En el mundo real, esto daría instantáneamente a una aplicación de la «vieja escuela» un aire moderno, acorde con las expectativas actuales de los usuarios impulsados por la IA. Muy interesante.
Otro objetivo era no modificar la estructura de datos ni alterar los mecanismos de interfaz de usuario existentes. En otras palabras, la actualización de la IA tenía que ser muy flexible, mínimamente intrusiva e idealmente reutilizable en otras aplicaciones heredadas.
En consecuencia, esta actualización no se basa en 4D.Vectors ni en incrustaciones. Volveremos sobre esto más adelante, pero vale la pena enfatizarlo ahora: introducir IA en su aplicación 4D no requiere necesariamente incrustaciones o búsqueda semántica. Son conceptos separados.
Prepararse
En 4D, ya hemos compartido varios ejemplos y webinars basados en IA, incluyendo demostraciones recientes sobre la construcción de un sistema RAG usando tool calling. Para este proyecto, decidí extraer el formulario de chat UI de la demoPeople & Skills y hacerlo más genérico y reutilizable.
La interfaz de usuario de chat consta de los siguientes componentes:
- El formulario AIChat, junto con su clase de formulario formAIChat.4dm. Este formulario proporciona:
- una entrada de texto simple para la pregunta del usuario
- un área web que muestra la conversación con el modelo
- La clase AI_ChatWithTools, responsable de:
- instanciar un bot AI usando el ayudante de chat del Kit 4D AI, basado en el servidor de inferencia y el modelo configurado en Resources/AIProvider.json
- cargar las herramientas definidas en Resources/AITools.json
- alojando la implementación de dichas herramientas
La clase se ejecuta completamente en modo streaming, por lo que el usuario obtiene una experiencia similar a ChatGPT, con respuestas que aparecen progresivamente.
- El singleton ChatHTMLRenderer se encarga derenderizar la conversación.
Toma la colección de mensajes almacenados en el AIBot y los convierte en HTML utilizando las plantillas ubicadas en la carpeta Resources:
-
- chat-template.html
- chat-template.css
- tool-icon.svg
Voy a ser honesto: el renderizado HTML fue esencialmente «vibe-coded» con la ayuda del agente GitHub de VS Code y Claude Sonnet 4.5. La parte más complicada fue garantizar la correcta renderización durante el streaming, especialmente para elementos como tablas, llamadas a herramientas y gráficos.
Llamadas a herramientas de IA
Como probablemente sepas, un modelo de IA no sabe nada inherentemente sobre tus datos. Sólo obtiene acceso a ellos a través de las herramientas que le proporcionas explícitamente. En esta sección, explicaré qué herramientas he implementado y cómo funcionan.
tool_getProducts, tool_getClients, tool_getInvoices, tool_getInvoiceLines
Todas estas herramientas se encargan de consultar la base de datos. Cada una realiza una simple consulta ORDA basada en los campos solicitados por el modelo.
Por supuesto hay un amplio campo de mejora en estas funciones de las herramientas. Gracias a las funcionalidades nativas de 4D 21 AI podríamos imaginar una búsqueda semántica utilizando 4D.Vector e incrustaciones para potenciar la recuperación de Clientes. Como esto implicaría cambios más profundos en la estructura, he optado por un camino más sencillo.
Desglosemos tool_getClients, ya que el mismo patrón se aplica a todas las demás herramientas «getter»:
Function tool_getClients($input : Object) : Object
Esta función recibe un objeto como entrada y devuelve un objeto como salida. La entrada describe los parámetros de búsqueda; la salida contiene los datos resultantes.
Validación de la entrada
var $validation; $returnObject : Object
var $entities : cs.CLIENTSSelection:=ds.CLIENTS.all()
$validation:=JSON Validate($input; This._getToolArgumentsSchema(This._functionName(Call chain)))
If (Not($validation.success))
return {error: "Could not validate input parameters against JSON Schema, call the tool again with proper input parameters"}
End if
El primer paso es validar que la entrada coincide con el esquema definido en AITools.json.
La mayoría de los modelos modernos producirán llamadas a la herramienta bien estructuradas, pero dependiendo del modelo, la validación sigue siendo una buena práctica de seguridad.
Si la validación falla, la herramienta devuelve un objeto simple que contiene una propiedad de error. Es importante saber que el modelo lee este valor, así que no dudes en ser explícito sobre lo que ha ido mal.
Establecer valores de entrada por defecto
$input.ID:=($input.ID) || "any"
$input.Name:=($input.Name) || "@"
$input.Contact:=($input.Contact) || "@"
$input.Total_Sales:=$input.Total_Sales || {}
$input.Total_Sales.min:=$input.Total_Sales.min || 0
$input.Total_Sales.max:=$input.Total_Sales.max || 9999999
$input.orderBy:=($input.orderBy) || {}
$input.orderBy.field:=($input.orderBy.field) || "Name"
$input.orderBy.order:=($input.orderBy.order) || "asc"
$input.top:=($input.top) || This.defaultTop
$input.countOnly:=($input.countOnly) || False
Los valores por defecto garantizan consultas ORDA seguras, pero también tienen un beneficio secundario interesante: escribir un esquema JSON completo en AITools.json puede ser tedioso. En su lugar, dejé que GitHub Copilot dedujera el esquema a partir del código y la estructura por defecto y el resultado fue excelente. Copilot produjo una definición precisa del esquema basándose únicamente en la lógica anterior.
Parámetros a tener en cuenta
- orderBy y top reducen el tamaño de la salida (y por tanto el uso de tokens).
- countOnly permite que el modelo solicite sólo el recuento de registros sin recuperar datos.
Ejemplos prácticos:
- Si el modelo necesita el número de clientes → countOnly = true
- Si quiere los 5 primeros clientes → top = 5 y orderBy: {field: «Total_ventas», order: «desc»}
Inicialización del objeto de salida
$returnObject:={}
$returnObject.form:="Clients"
$returnObject.dataClass:="CLIENTS"
$returnObject.counts:={}
$returnObject.counts.total:=$entities.length
- form indica qué formulario de interfaz de usuario se puede utilizar para mostrar los resultados – lo veremos más adelante.
- dataClass identifica la clase de datos implicada, importante para consultar varias tablas – lo veremos más adelante.
- counts.total es el número de registros antes de aplicar cualquier filtro.
Ejecución de la consulta
If ($input.ID#"any")
$entities:=$entities.query("ID = :1"; $input.ID)
End if
$entities:=$entities.query("Name = :1 and Contact = :2 and Total_Sales >= :3 and Total_Sales <= :4 order by "+$input.orderBy.field+" "+$input.orderBy.order; $input.Name; $input.Contact; $input.Total_Sales.min; $input.Total_Sales.max)
Esta parte es sencilla y podría hacerse más genérica, pero no era mi objetivo.
Finalización del resultado
$returnObject.counts.totalFiltered:=$entities.length
If ($returnObject.counts.totalFiltered>$input.top)
$entities:=$entities.slice(0; $input.top)
End if
$returnObject.counts.totalSent:=$entities.length
$returnObject.entities:=($input.countOnly) ? [] : $entities.toCollection("ID, Name, Contact, City, State, Country, Discount_Rate, Total_Sales, Comments")
return $returnObject
Desglose
- counts.totalFiltered: número de registros después del filtrado
- counts.totalSent: número realmente devuelto (tras aplicar top)
- entities: la colección de datos final (o vacía si countOnly = true)
Observará que la herramienta controla exactamente qué atributos se envían al modelo. Conseguir el equilibrio adecuado es importante.
Lo que hay que tener en cuenta
- Calidad de la conversación: más datos → mejores conocimientos.
- Rendimiento: más datos → mayor tiempo de procesamiento
- Ventana contextual: los modelos locales pueden degradarse especialmente cuando están sobrecargados
- Costes: más tokens → mayor uso de los servidores de inferencia en la nube
- Confidencialidad: tenga siempre en cuenta la GDPR y la sensibilidad de los datos al elegir los campos
El resultado final
Sea cual sea el objeto devuelto por la herramienta, 4D AI Kit lo serializa de forma transparente y lo entrega al modelo. Muy práctico, ¿verdad?
Consultas cruzadas entre tablas y las primeras indicaciones del sistema
Otro descubrimiento interesante durante este proyecto fue que el modelo no entendía automáticamente las relaciones entre tablas. Por ejemplo, preguntas como
Dame los 5 clientes más importantes, su factura más alta y su producto más pedido
no siempre producían resultados fiables.
El problema era sencillo: el modelo no se daba cuenta de que el ID de la tabla Clientes debía utilizarse para consultar la tabla Facturas, o de que las líneas de factura estaban relacionadas con los productos, etcétera.
En otras palabras, necesitaba dotar al modelo de cierto conocimiento sobre la estructura, pero sin ir demasiado lejos. Mis requisitos eran
- Que el sistema no me pidiera nada: No quiero actualizar manualmente el aviso cada vez que cambie la estructura.
- Ningún catálogo completo de la estructura: El modelo no necesita el esquema completo, sólo las relaciones relevantes.
Para conseguirlo, he reutilizado una clase muy útil de Thomas Maul: StructureInfo.4dm, extrayendo sólo las relaciones entre tablas.
A partir de ella, generé mi prompt inicial del sistema:
var $relations : Collection:=This._relationsInfos()
$systemPrompt:="You are a helpful assistant. I need your help to answer questions data stored in my application.\n"+\
"**CONTEXT**\n"+\
"The application stores data about invoices, products and clients"+\
"In some cases, you'll need to cross query several tables (dataClasses) in order to answer."+\
"To help you, here are the application relations between tables (dataClasses):\n"+\
JSON Stringify($relations)+"\n"+\
"**INSTRUCTIONS**\n"+\
"Analyze questions and answer step by step.\n"+\
"Use the tools at your disposal to answer everytime you think they are relevant.\n"+\
"**FORMATING**\n"+\
"Use HTML everytime.\n"+\
"Use bullet lists and Tables everytime everytime necessary\n"+\
"**IMPORTANT**\n"+\
"When calling tools, always include all required arguments in valid JSON.\n"+\
"Do not call a tool with empty arguments. If a value is missing, choose a reasonable default.\n"+\
"Always double check tools results before answering. Especially when they rely on vector search. \n"+\
"Indeed they may return results not matching with your search intention.\n"+\
"When tool calling returns data not related with the initial question, or that you cannot use to answer,\n"+\
"avoid detailing such results too much and stay short.\n"
Combinando estos metadatos de relación con la información dataClass devuelta por cada herramienta, el modelo pudo por fin entender cómo encadenar llamadas a herramientas a través de las tablas.
Esto mejoró drásticamente la calidad y la precisión de las respuestas del modelo, especialmente en el caso de análisis de varias tablas, como:
principales clientes por ingresos
o
productos pedidos con más frecuencia
o
mayores facturas por cliente
Experiencia del usuario: abrir un formulario 4D directamente desde la interfaz de usuario conversacional
En mis anteriores experimentos con RAG, a menudo mostraba los resultados en un cuadro de lista independiente. Este enfoque funcionaba, pero tenía varios inconvenientes:
- Necesitaba que el modelo proporcionara, en una sección oculta separada, la lista de ID de entidades para mostrar en el cuadro de lista. Esto significaba más tokens, respuestas más lentas y trucos HTML adicionales (como ocultar la lista de ID dentro de un bloque comentado).
- Requería trabajo adicional para hacer la lista lo suficientemente genérica como para manejar diferentes tipos de entidades (Clientes, Productos, Facturas, …).
- Limitaba la interfaz de usuario y parecía menos integrada en la aplicación.
Para este proyecto, quería algo más simple, más elegante y más natural para el usuario.
Así que adopté una nueva estrategia: cada vez que la IA menciona una entidad, genera un hipervínculo personalizado que abre directamente el formulario 4D correspondiente, filtrado en la entidad seleccionada.
Esto resultó ser sorprendentemente fácil. Simplemente añadí las siguientes instrucciones al prompt del sistema:
"**CUSTOM URL HANDLING**\n"+\
"Tools responses give information about the form to open when available, the dataClass and entities ID\n"+\
"When you display any element coming from a tool response, you must use a custom url so that the user can open the corresponding form\n"+\
"Such custom url must follow the following syntax examples:\n"+\
"<a href=\"myapp://openform?form=Products&dataClass=PRODUCTS&entities=989511\">A single product</a>\n"+\
"<a href=\"myapp://openform?form=Invoices&dataClass=INVOICESS&entities=654KJY,6467HGS,79864JSD\">A list of invoices</a>\n"
Como mis herramientas siempre devuelven el formulario apropiado, la clase de datos y la lista de entidades, el modelo tiene todo lo que necesita para generar estos enlaces correctamente. Los modelos de IA son muy buenos en este tipo de salida estructurada.
Gestión de las URL personalizadas en 4D
Todo lo que necesitaba en 4D era un pequeño mecanismo de filtrado de URLs en el área web para interceptar los enlaces myapp:// y abrir el formulario correcto con la selección de entidades correcta:
Function webAreaEventHandler($formEventCode : Integer)
var $formToOpen : Text
var $entitySelectionToShow : 4D.EntitySelection
var $queryObject : Object
Case of
: ($formEventCode=On Load)
ARRAY TEXT($filters; 0)
ARRAY BOOLEAN($allowDeny; 0)
APPEND TO ARRAY($filters; "myapp://*") // Intercept all URLs starting with myapp://
APPEND TO ARRAY($AllowDeny; False) //Allow
WA SET URL FILTERS(*; "Web Area"; $filters; $allowDeny)
: ($formEventCode=On URL Filtering)
$url:=WA Get last filtered URL(*; "Web Area")
// Parse the URL to determine what to do
Case of
: ($url="myapp://openform?@")
$queryObject:=This.queryObjectFromUrl($url)
If ($queryObject=Null)
return
End if
If ($queryObject.entitiesCollection.length>0)
$entitySelectionToShow:=ds[$queryObject.dataClass].query("ID in :1"; $queryObject.entitiesCollection)
CALL WORKER("Generic"; "W_Generic"; $queryObject.form; True; $entitySelectionToShow)
Else
CALL WORKER("Generic"; "W_Generic"; $queryObject.form; False)
End if
End case
End case
El resultado
El resultado final es una experiencia de usuario muy fluida, en la que la interfaz de usuario conversacional se integra perfectamente con el resto de la aplicación 4D.
Los usuarios pueden chatear con la IA, explorar datos y abrir instantáneamente formularios 4D nativos, todo ello sin alterar la lógica o estructura central de la aplicación.
Un modelo de interacción moderno, situado justo encima de una aplicación 4D clásica… sin apenas intrusión.
Una herramienta adicional: tool_createInvoice
La base de datos de muestra proporcionada en el proyecto de GitHub incluía sólo un puñado de facturas, no lo suficiente como para que las interacciones con la IA fueran interesantes. Necesitaba más datos.
Crear un formulario personalizado o escribir otra rutina de generación de facturas me parecía tedioso e irrelevante para el propósito de esta demostración. Y entonces me di cuenta: Ya tenía todo lo que necesitaba.
- Una interfaz de usuario conversacional.
- Un agente de IA consciente de mi modelo de datos.
- Conocimiento de clientes, productos, precios, relaciones…
Entonces, ¿por qué no dejar que la IA creara facturas directamente desde la interfaz de conversación?
Todo lo que tenía que hacer era añadir una nueva herramienta: tool_createInvoice.
Mi planteamiento era sencillo:
- Implementar una función sencilla de creación de facturas dentro de AI_ChatWithTools.
- Envolver todo el proceso en una transacción:
- En caso de error: retroceder y enviar un mensaje de error claro al modelo.
- En caso de éxito: confirma la factura y devuelve sus detalles.
- Documenta cuidadosamente el esquema de la herramienta, especialmente sus parámetros de entrada.
- Luego… deja que GitHub Copilot genere el esquema JSON dentro de AITools.json.
¿El resultado?
Ahora puedo pedirle a la IA que genere una factura completamente nueva por sí sola. Encuentra un cliente relevante, selecciona productos, calcula totales y llama a la herramienta, todo a través de la misma interfaz de usuario conversacional que construí anteriormente.
Resultó ser una forma sorprendentemente elegante de generar datos de prueba realistas.
Una observación interesante: algunos modelos (en particular OpenAI GPT-4.1) tienden a pedir confirmación, o al menos suficientes detalles de entrada, antes de crear realmente una factura… a menos que les indiques explícitamente que no lo hagan.
Representación HTML de los mensajes OpenAI
Como se mencionó anteriormente, esta parte fue completamente codificada por vibe. Reutilízala, adáptala o ignórala, lo que mejor se adapte a tus necesidades. La idea es simple: tomar una colección de mensajes OpenAI y renderizarlos en HTML.
Pero, por supuesto, nuestros usuarios finales siempre merecen algo más que un volcado básico de texto. Así que he añadido algunas características de calidad de vida:
- Un diseño limpio y estructurado para las llamadas a herramientas, incluyendo una indicación visual mientras se ejecuta una herramienta.
- Un botón «Copiar» para copiar fácilmente cualquier mensaje del asistente y pegarlo en un documento, ya sea Microsoft Word, Google Docs o un correo electrónico.
Esto hace que toda la experiencia conversacional resulte pulida y profesional, a la vez que rápida y ligera de implementar.
- Un botón de copia para cada tabla, que permite al usuario extraer instantáneamente los datos tabulares sin procesar y pegarlos directamente en una hoja de cálculo tipo Excel.
Esto resulta muy práctico cuando los usuarios quieren manipular o comparar datos fuera de la aplicación sin exportar nada.
Capacidades de creación de gráficos directamente dentro de la interfaz de usuario conversacional
Al conversar con una IA sobre facturas, clientes o productos, los usuarios esperan naturalmente gráficos. Clasificaciones, comparaciones, tendencias… las perspectivas visuales forman ahora parte de cualquier experiencia moderna.
Así que decidí introducir la representación de gráficos en línea en la interfaz de chat y resultó ser sorprendentemente fácil, gracias a un poco de vibe-coding con GitHub Copilot.
Lo implementé en tres pasos:
1) Importar Chart.js dentro de chat-template.html y extender mi motor de renderizado HTML para reconocer bloques <chart>…</chart>.
Mientras que la IA está transmitiendo su respuesta, una pequeña animación CSS muestra un marcador de posición gráfico, dando una sensación UX pulido.
2) Ampliar el mensaje del sistema para enseñar explícitamente a la IA cómo solicitar gráficos. He añadido instrucciones como
"**CHARTS**\n"+\
"Create charts for rankings, comparisons, trends, or distributions. Format: <chart>{...JSON...}</chart>\n"+\
"Available types: bar, line, pie, doughnut, radar, polarArea. Always include:\n"+\
"- \"type\": chart type\n"+\
"- \"data.labels\": array of x-axis labels\n"+\
"- \"data.datasets\": array with \"label\", \"data\" (numeric array), \"backgroundColor\" (color array)\n"+\
"- \"options.responsive\": true\n"+\
"- \"options.plugins.title\": {\"display\": true, \"text\": \"Chart Title\"}\n"+\
"- \"options.scales.y.beginAtZero\": true (for bar/line charts)\n"+\
"Use distinct vibrant colors (e.g., #4caf50, #2196f3, #ff9800, #e91e63, #9c27b0). Set \"legend.display\" to false for single datasets, true for multiple.\n"
3) Dejar que la IA genere definiciones de gráficos en JSON, que el área web renderiza al instante utilizando Chart.js.
El resultado es realmente impresionante: el modelo produce imágenes significativas directamente en el flujo de la conversación, lo que enriquece enormemente la experiencia.
Dicho esto, yo no confiaría totalmente en el modelo para cálculos complejos. Para el uso en producción, sería prudente añadir una serie de herramientas dedicadas a:
- totales y acumulaciones
- agrupaciones y agregaciones
- resúmenes temporales
- comparaciones de periodos
- extracción de KPI
4D realizaría los cálculos difíciles y precisos, y la IA simplemente los solicitaría y dibujaría los gráficos basándose en datos numéricos precisos y validados.
Una combinación perfecta de fiabilidad 4D e interfaz de usuario potenciada por la IA.
Conclusión
¿Y ahora qué?
Básicamente, hemos tomado una aplicación empresarial algo anticuada y la hemos dotado de una experiencia moderna, conversacional e impulsada por IA.
Pero verlo simplemente como «otra forma más de obtener datos» sería no entenderlo.
Claro, no se necesita una interfaz de usuario conversacional para obtener «los 5 principales clientes por ventas totales en 2025».
Una pantalla básica con un par de filtros puede hacerlo.
Pero enseguida te das cuenta de que esto es otra cosa.
Es una nueva forma de explorar tus datos.
Con un único formulario, la interfaz de chat, los usuarios pueden preguntar cualquier cosa: facturas, productos, clientes… ya quieran una tabla, un gráfico o ambos.
Esto elimina la necesidad de construir innumerables pantallas especializadas para necesidades de información ligeramente diferentes.
Y una vez que lo veas, comprenderás también que esto va mucho más allá de consultar y mostrar datos.
El valor real no está en responder «muéstrame esto».
El valor real está en responder «ayúdame a entender esto».
Tu pregunta ya no debería ser:
Dibuja un gráfico comparando 2024 y 2025
sino:
Proporcione un informe de ventas detallado para 2025, que incluya las ventas totales, los 5 productos principales por ingresos, los 5 clientes principales por ventas, la tendencia de las ventas a lo largo del año y las facturas impagadas. Muestre gráficos y tablas relevantes y explique cualquier tendencia, oportunidad o riesgo destacable. Asimismo, compare el rendimiento de los productos con respecto a 2024 y sugiera ideas prácticas para mejorar las ventas y la tesorería. Incluya una revisión del rendimiento por cliente y por producto con métricas clave. Quiero tanto cifras como explicaciones y perspectivas prácticas. El informe incluirá comparaciones entre 2024 y 2025. Los objetivos de dicho informe son: cómo aumentar las ventas, a qué clientes dirigirse, cómo aumentar el rendimiento de los productos.
Y este es el tipo de respuesta que usted -y sus usuarios- obtendrán:
Una interfaz conversacional única, perfectamente integrada en su aplicación 4D, capaz de comprender, razonar, visualizar, crear…
Esto no es una funcionalidad.
Es un paso hacia una nueva generación de aplicaciones de negocio.
¿Cómo reutilizar esto en su propio proyecto?
Si la lectura de este post y jugar un poco con la aplicación despertado un interés, puede simplemente:
- Copiar los siguientes archivos en tu proyecto:
- Clases
- AI_ChatConHerramientas.4dm
- ChatHTMLRenderer.4dm
- formAIChat.4dm
- StructureInfo.4dm
- Formulario
- AIChat
- Recursos
- AIprovider.json
- AITools.json
- chat-template.html
- chat-plantilla.css
- icono-herramienta.svg
- Realiza las adaptaciones necesarias
- Indicación del sistema e implementación de herramientas en AI_ChatWithTools.4dm
- Definición de las herramientas posteriores en AITools.json (¡utiliza AI para esto!)
- Adapta la apertura del formulario a tu aplicación en formAIChat.webAreaEventHandler()
- Configuración del proveedor de AI en AIprovider.json
- Clases
Y ya está.
Termino con un par de observaciones adicionales:
En este ejemplo, toda la lógica de las herramientas se implementa dentro de la clase AI_ChatWithTools. Lo hice así porque mi objetivo era ser lo menos intrusivo posible, y porque el proyecto no implementa DataClasses. Dependiendo de la arquitectura de tu base de código, una mejor práctica podría ser tener dichas herramientas apuntando a funciones DataClasses.
Todo este proyecto funciona perfectamente con OpenAI y el modelo gpt-4.1 o superior. ¡Trabajar con un modelo local plantea retos adicionales que pueden ser discutidos en el foro 4D!


