Translate

jueves, 15 de noviembre de 2018

Inteligencia Artificial. Eliza: Dialogo con una máquina


Nota del T: Sigo traduciendo el libro de Peter Norvig. Son complicadas algunas funciones, le aconsejo que las vaya construyendo a medida que lee. Si hay problemas con alguna traducción les ruego me avisen.

Acá dejo el programa completo: ELIZA.lisp

Este capítulo y el resto de la parte 1 examina tres programas más de IA bien conocidos de la década de 1960. Eliza mantiene una conversación con el usuario en la que es simulada una psicóloga. STUDENT resuelve problemas de los que son encontrados en los libros de álgebra, y MACSYMA resuelve una variedad de problemas matemáticos, incluyendo cálculo diferencial e integral. Desarrollaremos versiones de los primeros dos programas que duplican la mayoría de las características esenciales, pero para el tercero implementaremos solo una pequeña parte de las capacidades del programa original.
Los tres programas usan una técnica llamada “coincidencia de patrones”. La parte 1 sirve para mostrar la versatilidad –y también las limitaciones- de esta técnica.
De los tres programas, los primeros dos procesos introducen un Español plano, y los últimos dos resuelven problemas no triviales de matemática, de esta manera hay algunas bases para describirlos como “inteligentes”. Por otro lado, veremos que esta inteligencia es por lejos una ilusión, y que Eliza en particular fue diseñada para demostrar esta ilusión, no para ser un programa “serio” de IA.
ELIZA fue uno de los primeros programas que tenía al idioma Inglés como salida tanto así como en la entrada. El programa fue llamado después la heroína de Pygmalion, que fue enseñada para hablar Inglés correctamente a través de un dedicado profesor. El principal desarrollador de ELIZA fue el profesor Joseph Weizenbaum del MIT, publicada en el documento sobre ELIZA en Enero del 1966 para el problema de la comunicación entre máquinas de cómputo. La introducción a ese documento es reproducida enteramente aquí:

Se dice que explicar algo es explicarlo en profundidad. Esta máxima está aquí para mostrar como es el área de programación de ordenadores, especialmente en lo que se concierne a programación heurística e inteligencia artificial. En estos dominios las maquinas son hechas para comportarse de maneras maravillosas, obteniendo suficiente para deslumbrar incluso a la mayoría de los observadores experimentados. Pero una vez que un programa particular es desenmascarado, una vez que el trabajo interno es explicado en un lenguaje suficientemente plano para inducir a la comprensión, su magia se desmorona, su puesto es revelado como una mera colección de procedimientos, cada uno de los cuales es comprensible. El observador se dice a si mismo, “Yo podría haber escrito eso”. Con ese pensamiento el mueve el programa en cuestión del estante marcado como “inteligente”, al de las curiosidades, encajando en la discusión solo con personas menos ilustradas que él.

El objeto de este documento es causar solo una re-evaluación del programa a ser “explicado”. Pocos programas necesitan eso.
Déjeme explicar el punto sobre que el objeto principal de este libro es causar que el lector se diga a sí mismo, “yo podría haber escrito eso”. Esta será la esperanza en algunos casos debido a que el lector haya ganado algo, y no solamente porque el programa en cuestión haya perdido su habilidad para deslumbrar.
Empecemos nuestra investigación sobre ELIZA mirando un ejemplo de diálogo desde el artículo de Weizenbaum. ELIZA fue diseñada para ser instruida por una variedad de guiones, o patrones de interacción. En efecto tiene varios guiones, incluyendo algunos en alemán y galés. Pero el único guión que fue usado para cualquier extensión fue en inglés, y esto hace ELIZA emular un psicoanalista Rogeriano. Los Rogerianos no tienen “directivas” – ellos intentan dejar al paciente que se revele a sí mismo. Este es un guion sencillo de seguir para ELIZA, porque este encierra el hecho de estar pasivo y reaccionar, antes que voluntariamente generar nueva información. En el listado siguiente, la entrada del usuario está en oraciones, tanto en mayúsculas como minúsculas, y ELIZA responde solo en mayúsculas:

Los hombres son todos iguales.
EN QUE SENTIDO
Ellos están siempre fastidiándonos sobre una cosa u otra.
PUEDES SER MAS ESPECIFICA
Bueno, mi novio me hizo venir aquí.
TU NOVIO TE HIZO VENIR AQUÍ
El me dice que estoy deprimida a menudo.
ME APENA ESCUCHAR QUE ESTES DEPRIMIDA
Es verdad. No soy feliz.
PIENSAS VENIR AQUÍ PARA QUE TE AYUDE A SER FELIZ.
Necesito alguna ayuda, eso me parece.
QUE PASARIA SI TE OFREZCO ALGUNA AYUDA
Tal vez aprendería a llevarme bien con mi madre.
HABLAME MAS ACERCA DE TU FAMILIA

Esto parece impresionante; el programa está respondiendo razonablemente a una variedad de entradas y muestra una verdadera “comprensión”. Pero, como explica Weizenbaum, esta apariencia es una ilusión, y el programa ha reducido la difícil tarea de comprender a través de un cuidadoso reconocimiento, transformando, y haciendo eco de las piezas de entrada. El procedimiento es observar patrones específicos, basados en una palabra clave o palabras en la entrada. Por ejemplo, siempre que el programa aparece la palabra “iguales” o “mismo”, esta escribe un mensaje en pantalla respondiendo sobre cosas que son semejantes, en este caso “EN QUE SENTIDO”. Otros patrones son más complicados. Cerca del último intercambio, ELIZA arroja lejos todo lo que esta después de la coma, y luego usa un patrón que mira las entradas de la forma “necesito X” y lo transforma en la salida “Que pasaría si te ofrezco X”, donde X es la frase “alguna ayuda” en este caso. La gran ventaja de este enfoque es que la respuesta se hará en base a una variedad de entradas: “necesito vacaciones” y se obtendrá “Que pasaría si te ofrezco vacaciones”. Esto ocurrirá automáticamente, sin tener que decirle al programa la diferencia entre “alguna ayuda” y “vacaciones”. La desventaja es que el programa algunas veces hará respuestas tontas: “necesito esto como un agujero en la cabeza” será respondido con “Que pasaría si te ofrezco esto como un agujero en la cabeza”. En efecto, el programa solo construye cualquier sentido en todo caso si recibe entradas desde alguien que construye sentido –y alguien que interpreta sus respuestas caritativamente. Cuando se le da “necesito gibble pliz zzq”, ELIZA replicaría “Que pasaría si te ofrezto gibble plix zzq”, sin ningún indicio de que algo está mal. Otro ejemplo del programa original ELIZA cuando mantiene una comunicación con otro programa PARRY, que fue escrito por el Dr. Kenneth Colby para simular un comportamiento paranoico. Parte de esa conversación sigue a continuación, con ELIZA nuevamente solo en mayúsculas y PARRY en oraciones, tanto en mayúsculas como minúsculas:

No entiendo tus motivaciones.
¿QUE NO ENTIENDES DE MIS MOTIVACIONES?
¿No debería ser yo?
¿POR QUE DICES ESO AHORA?
Así es la manera.
SUPONGA QUE LA MANERA NO FUERA ASI.
¿Así?
NO ESTOY SEGURA DE ENTENDERTE COMPLETAMENTE.
Insistes demasiado en las mismas preguntas.
¿QUE TE SUGIERE ESA SEMEJANZA?
No te entiendo.
¿QUE ES LO QUE DESEAS ENTENDER?

Esto sugiere que PARRY el vecino de ELIZA, tiene una comprensión profunda del diálogo. Cuando hay un humano que tiene la mayoría de la carga para llevar adelante la conversación, estos programas pueden ser convincentes, pero cuando el humano está fuera de la conversación, las debilidades del programa son reveladas.

Describiendo y especificando ELIZA


Ahora que tenemos una idea de lo que es ELIZA, podemos comenzar la descripción y especificación del programa, y eventualmente movernos hacia la implementación y el debugging. El algoritmo de ELIZA puede ser descrito simplemente como: (1) leer una entrada, (2) buscar un patrón que coincida con la entrada, (3) transformar la entrada en una respuesta, y (4) mostrar en pantalla la respuesta. Estos cuatro pasos son repetidos para cada entrada.
La especificación e implementación de los pasos (1) y (4) son triviales: para (1), usa la función interna read para leer una lista de palabras, y para (4) usa print para mostrar en pantalla la lista de palabras en la respuesta.
Por supuesto, hay algunos inconvenientes para esta especificación. El usuario tendrá que escribir una lista real –usando paréntesis- y el usuario no puede usar caracteres especiales para leer, tales como comillas, comas y períodos. Así nuestra entrada no puede estar sin restricciones tal como el dialogo del ejemplo, pero ese es un pequeño precio a pagar para la conveniencia de tener la mitad del problema resuelto.

Coincidencia de patrones


La parte difícil está en los pasos (2) y (3) –esta noción de coincidencia de patrones y transformación. Hay cuatro cosas relacionadas: un patrón general y la respuesta, y una entrada específica y la transformación de esa entrada. Desde que accedimos a representar las entradas como una lista, esto permite tratar a otros componentes como listas también. Por ejemplo, podemos tener:

Patrón: (necesito X)
Respuesta: (¿Qué pasaría si te ofrezco X?)
Entrada: (necesito vacaciones)
Transformación: (¿Qué pasaría si te ofrezco vacaciones?)

El programa de coincidencia de patrones debe hacer coincidir los literales i con i, necesito con necesito, y a con a, como así también hacer coincidir la variable X con vacaciones. Esto presupone que hay algunas maneras de decidir que X sea una variable y que necesito no lo es. Debemos arreglar el sustituto vacaciones para X con la respuesta, para obtener la transformación final.
Ignorando por un momento el problema de transformar el patrón en la respuesta,  podemos ver que esta noción de coincidencia de patrones es solo una generalización de la función Lisp equal. Primero mostramos la función simple-equal, que es como la función interna equal, y la función patron-coincide que es definida para manejar variables de coincidencia de patrones:

(defun simple-equal (x y)
  "¿Son x e y iguales? (No hace revision dentro de cadenas.)"
  (if (or (atom x) (atom y))
      (eql x y)
    (and (simple-equal (first x) (first y))
                 (simple-equal (rest x) (rest y)))))

(defun patron-coincide (patron entrada)
  "¿Hay coincidencia entre patron y entrada? Cualquier variable puede coincidir con cualquier cosa."
  (if (variable-p patron)
      t
    (if (or (atom patron) (atom entrada))
                (eql patron entrada)
      (and (patron-coincide (first patron) (first entrada))
                   (patron-coincide (rest patron) (rest entrada))))))


Antes de que podamos avanzar, necesitamos decidir sobre una implementación para las variables de patron-coincide. Podríamos, por instancia, decir que solo un cierto conjunto de símbolos, tales como {X,Y,Z} son variables. Alternativamente, podríamos definir una estructura de tipo de variable, pero entonces no tendríamos que escribir algo como (make-variable :name ‘X) cada vez que buscamos una. Otra opción podría ser el uso de símbolos, pero para distinguir variables de constantes por el nombre del símbolo.  Por ejemplo, en Prolog, las variables comienzan con letras mayúsculas y las constantes con minúsculas. Pero Common Lisp no es sensible al contexto, así que esto no funcionará. En lugar de ello, hay una tradición en los programas de IA basados en Lisp donde las variables son símbolos que comienzan el carácter de pregunta. Hasta aquí hemos repartido símbolos como átomos –objetos sin estructura interna. Pero las cosas son siempre más complicadas de lo que aparentan al principio y, tanto en Lisp así como en física, lo que provoca que incluso los átomos tengan componentes. En partículas, los símbolos tienen nombres, que son cadenas de caracteres y son accesibles a través de la función symbol-name. Las cadenas en cambio tienen elementos que son caracteres, accesibles a través de la función char. El carácter ‘7’ es denotado por la secuencia de auto-evaluación #\? De esta manera el predicado variable-p puede ser definido como sigue a continuación, y ahora tenemos un programa completo de patrón de coincidencias:

(defun variable-p (x)
  "¿x es una variable? (un simbolo que comienza con '?')"
  (and (symbolp x) (equal (char (symbol-name x) 0) #\?)))

> (patron-coincide '(necesito ?X) '(necesito vacaciones))
T
> (patron-coincide '(necesito ?X) '(realmente necesito vacaciones))
NIL

En cada caso obtenemos la respuesta correcta, pero no obtenemos ninguna indicación de lo que es  ?X, de esta manera no podríamos sustituirla dentro de la respuesta. Necesitamos modificar patron-coincide para que devuelva algún tipo de tabla de variables y sus correspondientes valores. En la construcción de esta opción, el programador experimentado en Common Lisp puede ganar algo de tiempo siendo oportunista: reconocer cuando hay una función existente que hará una gran parte de la tarea. Lo que buscamos es sustituir valores para variables a través de la respuesta. El programador atento haría referencia al índice de este libro o de un manual de referencia de Common Lisp y buscará las funciones substitute, subst y sublis. Todas estas sustituyen algunas expresiones nuevas por unas anteriores con una expresión. Sublis en cambio es la más apropiada porque es la única que nos permite hacer varias sustituciones al mismo tiempo. Sublis toma dos argumentos, el primero es una lista de pares anterior-nuevo, y el segundo es una expresión en la que hace las sustituciones. Para cada uno de los pares, el car es reemplazado por el cdr. En otras palabras, formaríamos cada par con algo como (cons anterior nuevo). (Así como una lista de pares es conocida como una lista de asociación, o a-list, debido a que esta asocia claves con valores. En términos del ejemplo abajo, usaríamos:

> (sublis '((?X . vacaciones))
                '(Que pasaría si te ofrezco  ?X ?))
(QUE PASARIA SI TE OFREZCO VACACIONES ?)

Nota: Quisiera poner el signo de pregunta ¿ de apertura, pero algunos interpretes no los tienen.

Ahora necesitamos arreglar nuestro patron-coincide para que devuelva una a-lisp, mejor que solo T para un resultado exitoso. Aquí tenemos un primer intento:

(defun patron-coincide (patron entrada)
  "¿Coincide el patron con la entrada? CUIDADO: versión con errores."
  (if (variable-p patron)
      (list (cons patron entrada))
    (if (or (atom patron) (atom entrada))
                (eql patron entrada)
      (append (patron-coincide (first patron) (first entrada))
                      (patron-coincide (rest patron) (rest entrada))))))

Esta implementación luce razonable: devuelve una a-list de un elemento si el patrón es una variable, y une las a-list si el patrón y la entrada son listas. Sin embargo, hay varios problemas. Primero, la verificación (eql patron entrada) puede devolver T, que no es una lista, de esta manera append se quejará. Segundo, el mismo test debe devolver nil, lo que indicaría una falla, pero este será tratado como una lista, y será unido al resto de la respuesta. Tercero, no podemos distinguir entre el caso donde el patron falla –y devuelve nil- contra el caso donde todo coincide, pero no hay variables, devolviendo una a-lista nula. (Este es el problema del semipredicado). Cuarto, queremos las uniones de variables para acceder –si ?X es usado dos veces en el patrón, no queremos hacer coincidir dos valores distintos en la entrada. Finalmente, patron-coincide es ineficiente para verificar tanto el primero como el resto de las listas, incluso cuando la primera parte correspondiente no coincide. (¿No es increíble que haya cinco errores en una función de siete líneas?)
Podemos resolver estos problemas aceptando dos convenciones. Primero, es más conveniente hacer que patron-coincide sea un verdadero predicado, de esta manera aceptamos que devuelve nil solo para indicar falla. Lo que ocurre es que necesitaremos un valor nil-anonimo para representar la lista vacía. Segundo, si estamos yendo hacia una consistencia de los valores de las variables, entonces el first tendrá que saber cuál es el rest. Podemos lograr esto pasando la lista de uniones como un tercer argumento para patron-coincide. Lo haremos como un argumento opcional, porque queremos poder decir simplemente (patron-coincide a b).
Para abstraer más allá de estas decisiones de implementación, definiremos las constantes falla y sin-uniones para representar los dos valores devueltos con problemas. La forma especial defconstant es usada para indicar que estos valores no cambiarán. (Es opcional tener nombres de variables que comienzan y terminan con asteriscos, pero esta convención usualmente no se usa para las constantes). El motivo es que los asteriscos gritan, “Cuidado! Puedo ser cambiado por algo fuera de este alcance léxico”. Las constantes, por supuesto, no pueden ser cambiadas.

(defconstant falla nil "Indica que patron-coincide falla")
(defconstant sin-uniones '((t . t))
  "Indica el éxito de patron-coincide, sin variables.")

Luego, abstraemos más alla con el uso de assoc, introduciendo las siguientes cuatro funciones:

(defun obtener-union(var uniones)
  "Busca un par (variable . valor) en una lista de uniones."
(assoc var uniones))

(defun union-val(union)
  "Obtiene el valor de una union."
  (cdr union))

(defun buscar(var uniones)
  "Obtiene el valor (para var) de una lista de uniones."
  (union-val (obtener-union var uniones)))

(defun extender-uniones (var valor uniones)
  "Agrega un par (var . valor) a una lista de uniones."
  (cons (cons var valor) uniones))

Ahora que las variables y las uniones están definidas, patron-coincide es fácil. Este consiste de cinco casos. Primero, si la lista de uniones es falla, entonces la coincidencia falla (debido a que algunas coincidencias previas deben haber fallado). Si el patrón es una sola variable, entonces la coincidencia devuelve lo que sea que coincida con la variable; ya sea la lista de uniones existente, una extendida, o falla. Luego, tanto el patron como la entrada son listas, primero llamaremos a patron-coincide recursivamente sobre el primer elemento de cada lista. Este devuelve una lista de uniones (o falla), la que usaremos para hacer coincidir con el resto de las listas. Este es el único caso que invoca a una función no trivial, asi que es una buena idea probar informalmente que la función terminará: cada una de las dos llamadas recursivas redice el tamaño tanto del patron como de la entrada, y patron-coincide verifica el caso de patrones atómicos y entradas, de esta manera la función como un todo debe devolver eventualmente una respuesta (a menos que patrón y entrada tengan un tamaño infinito). Si ninguno de estos cuatro casos ocurre, entonces la coincidencia falla.

(defun patron-coincide (patron entrada &optional (uniones sin-uniones))
  "Coincidencia de patrones preparando la entrada en el contexto de las uniones"
  (cond ((eq uniones falla) falla)
                ((variable-p patron)
                 (coincidir-variable patron entrada uniones))
                ((eql patron entrada) uniones)
                ((and (consp patron) (consp entrada))
                 (patron-coincide (rest patron) (rest entrada)
                                                 (patron-coincide (first patron) (first entrada)
                                                                                 uniones)))
                (t falla)))
 
(defun coincidir-variable (var entrada uniones)
  "Coincide VAR con entrada? Usa (o actualiza) y devuelve las uniones."
  (let ((union (obtener-union var uniones)))
    (cond ((not union) (extender-uniones var entrada uniones))
                  ((equal entrada (union-val union)) uniones)
                  (t falla))))

Ahora podemos probar patron-coincide y ver cómo trabaja:

> (patron-coincide '(necesito ?X) '(necesito vacaciones))
((?X . VACACIONES) (T . T))

La respuesta es una lista de uniones de variable que esta dotadas de notación de pares; cada elemento de la lista es una par (variable . valor). (T . T) es un residuo de sin-uniones. Esto no hace daño realmente, pero podemos eliminarlo haciendo extender-uniones un poco más complicada:

(defun extender-uniones(var valor uniones)
  "Agrega un par (var . value) a la lista de uniones."
  (cons (cons var valor)
                ;; Una ver que agregamos una union "real",
                ;; podemos eliminar el tonto sin-uniones
                (if (eq uniones sin-uniones)
                    nil
                  uniones)))

> (sublis (patron-coincide '(necesito ?X) '(necesito vacaciones))
'(que pasaria si te ofrezco ?X ?))
(QUE PASARIA SI TE OFREZCO VACACIONES ?)
> (patron-coincide '(necesito ?X) '(realmente necesito vacaciones))
NIL
> (patron-coincide '(esto es facil) '(esto es facil) )
((T. T))
> (patron-coincide '(?X es ?X) '((2 + 2) es 4))
NIL
> (patron-coincide '(?X es ?X) '((2 + 2) es (2 + 2)))
((?X 2 + 2))
> (patron-coincide '(?P necesito . ?X)  '(yo necesito unas largas vacaciones))
((?X UNAS LARGAS VACACIONES) (?P . YO))

Nota del T: El problema con el español es que las conjugaciones del verbo dependen de persona y numero.

Note la distinción entre NIL y ( (T . T) ). El último tratamiento es exitoso, pero no hubieron uniones para devolver. Recuerde también que (?X 2 + 2) es lo mismo que (?X . (2 + 2)).
Una implementación más poderosa de patron-coincide se da en el capítulo 6. Otra implementación es dada más adelante. Es más eficiente pero más incómoda de usar.

Coincidencia de Segmentos  Patrones


En el patrón (?P necesito . ?X), la variable ?X coincide con el rest de la lista de entrada, sin importar cuál es su longitud. Esto está en contraste con ?P, que puede solo coincidir con un solo elemento, llamado, el primer elemento de la entrada. Para muchas aplicaciones de coincidencia de patrones, esto está bien; buscamos solamente hacer coincidir los elementos correspondientes. Sin embargo, ELIZA es algo diferente en que necesita llevar una cuenta para variables en cualquier posición que coincida una secuencia de ítems en la entrada. Llamaremos a tales variables de segmento. Necesitaremos una notación para diferenciar variables de segmento de las variables normales. Las posibilidades caen en dos clases: ya sea que usemos átomos para representar variables de segmento y distinguirlas por alguna convención ortográfica (como hicimos para distinguir variables de constantes) o que usemos construcciones no atómicas. Elegiremos la última, usando una lista de la forma (?* variable) para denotar variables de segmento.
El símbolo ?* es elegido porque combina la noción de variable con la notación de estrella de Kleene. De esta manera, el comportamiento que buscamos para patron-coincide es ahora:

> (patron-coincide  '((?* ?p) necesitamos (?* ?X))
                                    '(El Sr Perez y yo necesitamos vacaciones))
((?P EL SR PEREZ Y YO) (?X VACACIONES))

En otras palabras, cuando el patrón y la entrada son listas y el primer elemento del patrón es una variable de segmento, la variable hará coincidir la parte inicial de la entrada, y el resto del patrón  se intentará coincidir con el resto. Podemos actualizar patron-coincide para llevar una cuenta para esto agregando una simple cláusula cond. Definir el predicado para verificar las variables de segmento es fácil también:

(defun patron-coincide (patron entrada &optional (uniones sin-uniones))
  "Coincidencia de patrones preparando la entrada en el contexto de las uniones"
  (cond ((eq uniones falla) falla)
                ((variable-p patron)
                 (coincidir-variable patron entrada uniones))
                ((eql patron entrada) uniones)
                ((segmento-patron-p patron)                       ;***
                 (segmento-coincide patron entrada uniones))      ;***
                ((and (consp patron) (consp entrada))
                 (patron-coincide (rest patron) (rest entrada)
                                                  (patron-coincide (first patron) (first entrada)
                                                                                  uniones)))
                (t falla)))

(defun segmento-patron-p (patron)
  "Es este un segmento patron: (?* var) . pat)"
  (and (consp patron)
       (comienza-con (first patron) '?*)))

(defun comienza-con (list x)
  "Es esta una lista cuyo primer elemento es x?"
  (and (consp list) (eql (first list) x)))

Para escribir segmento-coincide, la pregunta importante es cuánto de la entrada va a coincidir con el segmento. Una respuesta es mirar el próximo elemento del patrón (el que está después de la variable de segmento) y ver que en esa posición eso ocurre en la entrada. Si no ocurre, el patrón total nunca puede coincidir, y debería fallar. Si eso ocurre, pasa a su posición. Si queremos hacer coincidir la variable preparando la parte inicial de la entrada, pasa por arriba. Pero primero tenemos que ver si el resto del patrón coincide con el resto de la entrada. Si esto es así se hace una llamada recursiva a patron-coincide. El resultado de esta llamada recursiva es llamado b2. Si b2 es exitoso, entonces vamos hacia adelante y hacemos coincidir la variable de segmento preparando la subsecuencia inicial.
El truco es que b2 falla. No queremos darlo completamente, porque puede ser que si la variable de segmento coincide con una subsecuencia más larga de la entrada, entonces el resto del patrón coincidiría con el resto de la entrada. De esta manera queremos intentar nuevamente la función segmento-coincide, pero forzándola a considerar una coincidencia más larga para la variable. Esto se hace introduciendo un parámetro opcional, inicio, que es inicialmente 0 y se incrementa con cada falla. Observe que esta política deja afuera la posibilidad de cualquier tipo de variable siguiendo  una variable de segmento. (Luego de remover esta restricción).

(defun segmento-coincide(patron entrada uniones &optional (inicio 0))
  "Coincide el segmento patron ((?* var) . pat) preparando la entrada."
  (let ((var (second (first patron)))
                (pat (rest patron)))
    (if (null pat)
                (coincidir-variable var entrada uniones)
      ;; Asumimos que pat comienza con una constante
      ;; En otras palabras, un patron no puede tener 2 vars consecutivas
      (let ((pos (position (first pat) entrada
                                                  :start inicio :test #'equal )))
                (if (null pos)
                    falla
                  (let ((b2 (patron-coincide pat (subseq entrada pos) uniones)))
                    ;;Si esta coincidencia falla, intenta otra mas larga
                    ;;Si esto funciona, verifica que las variables coincidan
                    (if (eq b2 falla)
                               (segmento-coincide patron entrada uniones (+ pos 1))
                      (coincidir-variable var (subseq entrada 0 pos) b2))))))))

Vemos algunos ejemplos de coincidencia de patrones:

> (patron-coincide '((?* ?p) necesitamos (?* ?x))
                                  '(El Sr Gomez y yo necesitamos vacaciones))
((?P EL SR GOMEZ Y YO) (?X VACACIONES))

> (patron-coincide '((?* ?x) es un (?* ?y)) '(que el es es un tonto))
((?X QUE EL ES) (?Y TONTO))

El primero de estos ejemplos muestra un caso bastante simple: ?P coincide con todo lo necesario, y ?X coincide con el resto. El siguiente ejemplo encierra el caso más complicado. Primero ?X coincide con todo lo que esta primero (esta es la posición 2, con la cuenta comenzando en 0 en Common Lisp). Pero luego el patrón falla al coincidir la entrada, de esta manera segmento-coincide intenta nuevamente iniciar con la posición 3. Esta vez todo funciona; es coincide con es, un coincide con un, y (?* ?Y) coincide con tonto.
Desafortunadamente, esta versión de segmento-coincide no hace coincidir todo como debería.
Considere el siguiente ejemplo:

> (patron-coincide '((?* ?x) a b (?* ?x)) '(1 2 a b a b 1 2 a b)) => NIL

Esto falla porque ?x coincide con la subsecuencia (1 2), y luego el resto del patrón coincide exitosamente con el resto de la entrada, pero al final la llamada coincidir-variable falla, porque ?X tiene dos valores diferentes. La solución es llamar a coincidir-variable antes de verificar que b2 falle, así nos aseguraremos intentar segmento-coincide nuevamente con una coincidencia larga sin importar cuál sea la causa de la falla.

(defun segmento-coincide (patron entrada uniones &optional (inicio 0))
  "Coincide el segmento patron (?* var) . pat) preparando la entrada."
  (let ((var (second (first patron)))
                (pat (rest patron)))
    (if (null pat)
                (coincidir-variable var entrada uniones)
      ;;Asumimos que pat comienza con una constante
      ;;En otras palabras, un patron no puede tener 2 vars consecutivas
      (let ((pos (position (first pat) entrada
                                                  :start inicio :test #'equal)))
                (if (null pos)
                    falaa
                  (let ((b2 (patron-coincide
                                    pat (subseq entrada pos)
                                    (coincidir-variable var (subseq entrada 0 pos)
                                                                               uniones))))
                    ;; Si esta coincidencia falla, intenta otra mas larga
                    (if (eq b2 falla)
                               (segmento-coincide patron entrada uniones (+ pos 1))
                      b2)))))))

Ahora vemos que la coincidencia ocurre:

> (patron-coincide '((?* ?x) a b (?* ?x)) '(1 2 a b a b 1 2 a b))
((?X 1 2 A B))

Observe que esta versión de segmento-coincide intenta hacer coincidir primero lo más corto posible. Podría también ser posible intentar hacer coincidir primero lo mas largo.

El programa ELIZA: Un traductor basado en reglas


Ahora que tenemos una coincidencia de patrones funcionando, necesitamos algunos patrones para hacer coincidir. Algo más, queremos que el patrón esté asociado con respuestas. Podemos hacer esto inventando una estructura de datos llamada regla, que consisten de un patrón y una o más respuestas asociadas. Hay reglas en el sentido en que ellas afirman, “Si tú ves A, entonces responde B o C, seleccionadas al azar.” Elegiremos la implementación más simple posible para las reglas: como listas, donde el primer elemento es el patrón y el resto es una lista de respuestas:

(defun regla-patron (regla) (first regla))
(defun regla-respuesta (regla) (rest regla))

Aquí un ejemplo de una regla:

(((?* ?x) quiero (?* ?y))
(que pasaria si te ofrezco ?y)
(por que quieres ?y)
(suponga que obtiene ?y pronto)

Nota del T: En español tenemos que usar verbos distintos para decir cosas distintas, en cambio en ingles un verbo tiene distintos significados. Por ejemplo  want en ingles puede decir varias cosas (querer, necesitar, desear). Pero en español debemos usar verbos distintos para decir “quiero” “necesito” “deseo”. También está el tema, no menor, del uso de las distintas conjugaciones de los verbos según la persona y el número.
Cuando es aplicada en la entrada (yo quiero verificar este programa), esta regla (cuando es interpretada por el programa ELIZA) debería replicar una respuesta aleatoria, sustituyendo en el valor de ?y, y respondiendo con (por que quieres verificar este programa). Ahora que sabemos lo que hará una regla individual, necesitamos decidir cómo manejar un conjunto de reglas. Si ELIZA está hecha para cualquier contexto, tendrá que tener una variedad de respuestas. Así varias reglas pueden ser aplicables a la misma entrada. Una posibilidad podría ser elegir una regla al azar desde un montón de reglas teniendo patrones que coinciden con la entrada.
Otra posibilidad es solo aceptar la primera regla que coincida. Esto implica que las reglas están en una lista ordenada, antes que un conjunto desordenado. La regla inteligente de ELIZA puede tomar ventaja de este ordenamiento y arreglo para la mayoría de las reglas específicas  come first, mientras más reglas vagas están cerca del fin de la lista.
El ELIZA original tiene un sistema donde cada regla tiene un número de prioridad asociado con ella. La regla de coincidencia con la más alta prioridad fue elegida. Observe que poniendo las reglas en orden logra el mismo efecto que tener un número de prioridad en cada regla: la primer regla implícita tiene la más alta prioridad, la segunda regla es la próxima más alta, y así sucesivamente. Hay una lista corta de reglas, seleccionadas del artículo original de Weizenbaum, pero con la forma de las reglas actualizadas a la forma que nosotros estamos usando. La respuesta al ejercicio 5.19 contiene una larga lista de reglas.

(defparameter *reglas-eliza*
'((((?* ?x) hola (?* ?y))
(Como estas. Por favor cuentame tu problema.))
(((?* ?x) quiero (?* ?y))
(que pasaria si te ofrezco ?y)
(por que quieres ?y) (suponga que obtienes ?y pronto))
(((?* ?x) si (?* ?y))
(realmente piensas que es probable que ?y) (deseas que ?y)
(que piensas sobre ?y) (realmente si ?y))
(((?* ?x) no (?* ?y))
(por que no?) (estas siendo un poco negativo)
(estas diciendo "NO" solo para ser negativo?))
(((?* ?x) era (?* ?y))
(eras tu realmente?) (tal vez ya sabia que tu eras ?y)
(por que me dices que tu eras ?y ahora?))
(((?* ?x) siento (?* ?y))
(te sientes a menudo ?y ?))
(((?* ?x) senti (?* ?y))
(que otros sentimientos tienes?))))

Finalmente estamos listos para definir a ELIZA apropiadamente. Como dijimos antes, el programa principal debería ser un bucle que lee entradas, las transforma, e imprime en pantalla el resultado. La transformación es hecha primariamente buscando alguna regla tal que su patrón coincida con la entrada, y luego sustituir las variables dentro de la respuesta de la regla. El programa es resumido a continuación en una tabla.
Hay unas complicaciones menores. Imprimiremos en pantalla un prompt para decirle al usuario que escriba algo. Usaremos la función aplanar para asegurarnos que la salida no tiene embebidas listas después de la sustitución de la variable. Un truco importante es alterar la entrada intercambiando “tu” por “mi” y así sucesivamente, ya que estos términos son relativos al que habla.

Funciones de alto nivel
eliza
Responde a la entrada del usuario usando reglas de coincidencia de patrones.
Variables Globales
*reglas-eliza*
Una lista de reglas de transformación.
Tipos de dato
regla
Una asociación de un patrón con una lista de respuestas.
Funciones
eliza
Responde a la entrada del usuario usando reglas de coincidencia de patrones.
usar-reglas-eliza
Busca alguna regla con la cual transformar la entrada.
cambiar-puntodevista
Cambia yo por tu y viceversa, y asi sucesivamente.
aplanar
Une juntos elementos (o listas) en la lista.
Funciones de Common-Lisp
sublis
Sustituye elementos dentro de un árbol.
Funciones definidas previamente
random-elt
Recoge un elemento aleatorio de una lista.
patron-coincide
Coincide un patrón con una entrada.
mappend
Aplica una función a cada elemento de la lista y une los resultados.

Aquí está el programa completo:

(defun eliza ()
  "Responde a la entrada del usuario usando reglas de coincidencia de patrones."
  (loop
   (print 'eliza>)
   (write (aplanar (usar-reglas-eliza (read))) :pretty t)))

(defun usar-reglas-eliza (entrada)
  "Busca alguna regla con la cual transformar la entrada."
  (some #'(lambda (regla)
                    (let ((resultado (patron-coincide (regla-patron regla) entrada)))
                      (if (not (eq resultado falla))
                                 (sublis (cambiar-puntodevista resultado)
                                                 (random-elt (regla-respuesta regla))))))
                *reglas-eliza*))

(defun cambiar-puntodevista (palabras)
  "Cambia yo por tu y viceversa, y asi sucesivamente."
  (sublis '((yo . tu) (tu . yo) (mi . ti) (soy . eres))
                  palabras))

(defun aplanar (la-lista)
  "Une juntos elementos (o listas) en la lista."
  (mappend #'mklist la-lista))

(defun mklist (x)
  "Devuelve x si este es una lista, en otro caso devuelve (x)."
  (if (listp x)
      x
    (list x)))

(defun mappend (funcion la-lista)
  "Aplica funcion a cada elemento de la lista y une los resultados."
  (apply #'append (mapcar funcion la-lista)))

(defun random-elt (opciones)
  "Recoge un elemento de una lista al azar."
  (elt opciones (random (length opciones))))

Observe el uso de write con la palabra clave :pretty. Este dará una mejor salida formateada en algunos casos. El programa hace uso de la función previamente definida random-elt, y aplanar, que son definidas aquí usando mappend y mklist, una función que es definida en el dialecto InterLisp pero no en Common Lisp.
El programa ELIZA actual tiene algunas complicaciones que no se muestran en esta versión. Primero, había un método alias para asociar varias palabras con el mismo patrón; tanto “madre” como “padre” podría ser asociado con el patrón “familia”.  Había también un mecanismo de sinónimo que podría tratar las palabras que signifiquen lo mismo. En el caso de entradas con varias frases separadas con coma, cada frase fue procesada en forma separada, y la respuesta con la prioridad más alta fue seleccionada. Adicionalmente, el ELIZA original tiene un mecanismo de  “memoria”. Cuando no coincide un patrón de entrada, esta dice algo como “Háblame más sobre X”, donde X es alguna entrada anteriormente dada. Finalmente, nuestra versión tiene pocas reglas. A pesar delos defectos de nuestra versión, esta puede ocasionalmente mantener una conversación como se muestra aquí:

> (eliza)
ELIZA> (hola)
(COMO ESTAS. POR FAVOR CUENTAME TU PROBLEMA.)
ELIZA> (quiero verificar este programa)
(QUE PASARIA SI TE OFREZCO VERIFICAR ESTE PROGRAMA)
ELIZA> (podria ver si funciona)
(REALMENTE PIENSAS QUE ES PROBABLE QUE FUNCIONA)
ELIZA> (no realmente)
(ESTAS DICIENDO "NO" SOLO PARA SER NEGATIVO?)
ELIZA> (no)
(ESTAS SIENDO UN POCO NEGATIVO)
ELIZA> (olvida eso estoy asombrado por el funcionamiento del programa)
(POR QUE ME DICES QUE TU ESTAS ASOMBRADO POR EL FUNCIONAMIENTO DEL PROGRAMA
 AHORA?)
ELIZA> (senti eso)
(QUE OTROS SENTIMIENTOS TIENES?)
ELIZA> (siento que ya es suficiente)
(TE SIENTES A MENUDO QUE YA ES SUFICIENTE ?)
ELIZA> :q
> 

Al final, lo que importa es la técnica, no el programa. ELIZA ha sido “explicada en profundidad” y podría legítimamente ser movida a la repisa de curiosidades. La coincidencia de patrones en general permanece como una técnica importante, y la veremos nuevamente en capítulos siguientes. La noción de un traductor basado en reglas  es importante también. El problema de entender español (y otros lenguajes) si siendo una parte importante en la IA. Claramente, el problema de entender español no fue resuelto por ELIZA. En la parte V, veremos el problema nuevamente, usando técnicas más sofisticadas.
Dejo un link con el eliza modificado; con mas reglas, agregue mas reglas si lo desea: Eliza2.lisp

Historia y Referencias



Como se mencionó antes, el artículo original describiendo ELIZA está en Weizenbaum 1966. Y otros sistemas de dialogo que usan técnicas similares a la coincidencia de patrones está en el PARRY de Kennth Colby (1975). Este programa simula la conversación de una persona paranoica de manera demasiado elemental para varios psicólogos profesionales. Aunque la técnica de coincidencia de patrones fue simple, el modelo es mantenido por sistemas mucho más sofisticados que ELIZA. Colby ha sugerido que los programas de dialogo como ELIZA, es aumentado con algunos modelos como PARRY, podrían ser herramientas útiles en el tratamiento de enfermedades mentales de personas. De acuerdo a Colby, podría ser barato y efectivo tener conversaciones con pacientes con un programa especialmente diseñado, que podría manejar casos simples y alertar a los doctores sobre los pacientes que necesitan más ayuda. El libro Poded de Ordenadores y Razones Humanas de Weizenbaum (1976) habla sobre ELIZA y PARRY y toma una visión muy crítica sobre la sugerencia de Colby. Otro trabajo interesante sobre modelos de sistemas de diálogo esta reportado por Allan Collins (1978) y Jamie Carbonell (1981).

lunes, 12 de noviembre de 2018

Programacion Web con AllegroServe

Breve introducción

Típicamente las páginas web son creadas en hipertexto (HTML) que le dicen al navegador como mostrar la pagina, incluyendo donde insertar imágenes y links hacia otras paginas. Por ejemplo una pagina en HTML luce asi:

<html>
   <head>
       <title>Hola</title>
   </head>

   <body>
      <p>Hola mundo!</p>
      <p>Esta es una imagen: <img src="imagen.gif"></p>
      <p>Este es un <a href="another-page.html">link</a> a otra pagina.</p>
   </body>
</html>



El navegador y el server se comunican usando un protocolo llamado Protocolo de Transferencia de Hipertexto (HTTP). Aunque no necesitas saber los detalles del protocolo es necesario entender que consiste de una secuencia de solicitudes iniciadas por el navegador y respondidas por el servidor. Esto es así, el navegador se conecta al servidor web y envía una solicitud que incluye al menos la URL deseada y la versión del protocolo HTML que utiliza el navegador. El navegador puede usar también datos en esta solicitud que le indican al servidor como mostrar el form HTML.

Para contestar una solicitud el servidor envía una respuesta que consta de un conjunto de cabeceras y un cuerpo. Las cabeceras contienen información sobre el cuerpo, como de que tipo de dato se trata (por ejemplo HTML, texto plano o una imagen), y el cuerpo es el dato mismo, que es mostrado por el navegador. El servidor puede enviar también una respuesta de error diciendo al navegador que esta solicitud no puede ser respondida por alguna razón. Una vez que el navegador ha recibido la respuesta completa desde el servidor, no hay comunicación entre ambos hasta la próxima vez que el navegador decide solicitar una pagina al servidor. Esto es lo principal sobre programación web, no hay manera de que el código se ejecute sobre el servidor para afectar lo que el usuario ve en su navegador sin que el navegador envíe una nueva solicitud al servidor. Algunas paginas web, llamadas estáticas, son simples archivos .html almacenados en el servidor web y ejecutadas cuando el navegador las solicita. Por otro lado, las páginas dinámicas consisten en HTML generado cada vez que la pagina es solicitada por el navegador. Por ejemplo una página dinámica puede ser generada a través de una consulta a una base de datos y luego construyendo el HTML que representa el resultado de la consulta. Cuando es generada la respuesta a una consulta, el código del lado del servidor tiene cuatro piezas principales de información. La primera pieza de información es la URL solicitada. Sin embargo, a menudo la URL es usada por el servidor web para determinar que código es responsable de generar la respuesta. Luego, si la URL contiene un signo de pregunta, todo lo que sigue al signo
de pregunta se considera como un "string de consulta", que es ignorada normalmente por el servidor web, excepto que esta disponible para la generación de código de la respuesta. La mayoria de las veces, la cadena de consulta contiene un conjunto de pares clave/valor. Las solicitudes desde el navegador también pueden contener "datos post", que también consisten usualmente en pares clave/valor. Los datos post son comúnmente usados para enviar formularios HTML. Los pares clave/valor suministrados tanto en la cadena de consulta como en los datos post son los llamados: "parámetros de consulta". Luego de haber enviado los string como una secuencia de solicitudes desde el navegador, el código ejecutándose en el servidor puede "configurar una cookie", enviando una cabecera especial en su respuesta al navegador, que contienen datos opacos llamados como "cookie". Después de que una cookie es configurada desde un servidor particular, el navegador enviará la cookie con cada solicitud enviada hacia ese servidor. Al navegador no le interesa los datos de la cookie, este solo regresa de vuelta hacia el servidor ese código a ser interpretado.
Hay 99% de elementos primitivos desde el lado del servidor. El navegador envía una solicitud, el servidor busca algún código para manejar la solicitud y ejecutarla, y el código usa tanto parámetros de consulta como cookies para determinar que hacer.

AllegroServe


AllegroServe esta incluido en la version de Allegro disponible desde Franz. AllegroServe provee un modelo de programación similar en espíritu a los Servlets de Java, tanto sobre archivos individuales como contenidos de un directorio. Cada vez que un navegador solicita una página, AllegroServe analiza la solicitud y observa un objeto, llamado "entidad", que maneja la solicitud. Algunas clases de entidad proveen como parte de AllegroServe información sobre como servir contenido estático. En otros casos, se ejecutara código Lisp arbitrario para generar la respuesta. La función start del paquete net.aserve arranca al servidor. Esta toma un numero de parámetros, pero el único que necesitas pasarle es :port, que especifica el puerto a ser activado. Si utilizas un sistema operativo derivado del Unix, probablemente deberías usar un puerto alto como 2001 en lugar del puerto por defecto para servidores HTTP, 80, debido a que en estos sistemas operativos solo el usuario root puede activar puertos por debajo de 1024. Para ejecutar AllegroServe activando el puerto 80 en Unix, necesitarías iniciar Lisp como root y luego usar los parámetros :setuid y :setgid para cambiar la identidad al iniciar después de abrir el puerto. Puedes arrancar un servidor activando el puerto 2001 de la siguiente forma:

> (use-package :net.aserve)
> (start :port 2001)
#<WSERVER port 2001 @ #x72511c72>

El servidor esta ahora corriendo en tu Lisp. Es posible que obtengas un error que diga algo sobre "port already in use" ("el puerto ya esta siendo utilizado") cuando intentes arrancar el servidor. Esto ocurre cuando el puerto 2001 está siendo utilizado por algún otro servidor en tu máquina. En ese caso, la manera más fácil de resolverlo es usar un puerto distinto, cambiando por otro valor en lugar de 2001 en las URLs usadas en este texto. Puedes continuar interactuando con Lisp a través del terminal debido a que AllegroServe arranca su propio hilo para manejar solicitudes desde navegadores. Esto ocurre, entre otras cosas, para que puedas usar el terminal Lisp para obtener una vista dentro de las entrañas de tu servidor mientras se está ejecutando, lo que permite corregir y testear una parte de manera más fácil que si el servidor fuera una completa caja negra. Asumiendo que estas ejecutando Lisp en la misma máquina que tu navegador, puedes chequear que ese servidor está activo y ejecutándose accediendo a la dirección en tu navegador http://localhost:2001/. En este punto deberías obtener un mensaje de error de página no encontrada en el navegador pues no tienes nada publicado aun. Pero el mensaje de error será desde AllegroServe; este lo mostrará en la parte superior de la página. Por otro lado, si el navegador mostrara un dialogo de error que diga algo como "La conexión fue rechazada cuando intentaba conectarse a localhost:2001", eso ocurre tanto si el servidor no se está ejecutando como así también cuando has iniciado con un puerto diferente del 2001.
Ahora puedes publicar algunos archivos. Supongamos que tienes un archivo hola.html en el directorio c:/servidor/ con el siguiente contenido: (Nota: Si utilizas Windows, el archivo del directorio que es "C:\Servidor\hola.html" se escribe en Lisp como "c:/Servidor/hola.html". Si el contenido se encontrara en la carpeta "Documentos" de un usuario llamado "RenePC" por ejemplo se accedería en Lisp así: "c:/Users/RenePC/Documents/hola.html")

<html>
   <head>
      <title>Hola</title>
   </head>

   <body>
      <p>Hola mundo!</p>
   </body>
</html>

Puedes publicar esto individualmente con la función publish-file:

> (publish-file :path "/hola.html" :file "c:/servidor/hola.html")
#<NET.ASERVE::FILE-ENTITY @ #x725eddea>

El argumento :path es el path que aparecerá en la URL solicitada por el navegador, mientras el argumento :file es el nombre del archivo en el sistema de archivos. Después de evaluar la expresión publish-file, puedes acceder a la dirección http://localhost:2001/hola.html, y debería mostrar una página como esta:



También puedes publicar el directorio entero c:/servidor/ (y todos sus subdirectorios) con la función publish-directory.

> (publish-directory :prefix "/" :destination "c:/Servidor/")
#<NET.ASERVE::DIRECTORY-ENTITY @ #x72625aa2>

En este caso, el argumento :prefix especifica el inicio del path de las URLs que deberían ser manejadas por esta entidad. De esta manera, si el servidor recibe una solicitud para http://localhost:2001/foo/bar.html, el path es c:/foo/bar.html. Este path es luego traducido al nombre reemplazando el prefijo "/" con el lugar de destino (destination) c:/tmp/html/. Así, la URL http://localhost:2001/hola.html será traducido en la solicitud por el archivo c:/servidor/hola.html.

Generando Contenido Dinámico con AllegroServe


Publicar entidades que generen contenido dinámico es casi tan simple como publicar contenido estático. Las funciones publish y publish-prefix son los análogos dinámicos de publish-file y publish-directory. La idea básica de estas dos funciones es que tu publicas una función que sera llamada para generar la respuesta a una solicitud tanto para una URL especifica como para cualquier URL con un prefijo dado. La función sera llamada con dos argumentos: un objeto representando la solicitud y la entidad publicada. La mayoría de las veces no necesitas hacer nada con el objeto entidad excepto para pasarle un par de macros de la que hablaremos en un momento. Por otro lado, usaras el objeto solicitud para obtener información enviada por los parámetros de consulta del navegador incluida en la URL o los datos post usando un formulario HTML.

Para un ejemplo trivial del uso de una función que genera contenido dinámico, se muestra una función que genera una pagina con un numero aleatorio distinto cada vez que es solicitada.

(defun numero-aleatorio (solicitud entidad)
   (with-http-response (solicitud entidad :content-type "text/html")
      (with-http-body (solicitud entidad)
         (format
            (request-reply-stream solicitud)
               "<html>~@
                  <head><title>Numero al azar</title></head>~@
                  <body>~@
                     <p>Numero Aleatorio: ~d</p>~@
                  </body>~@
                </html>~@
               "
               (random 1000)))))

Las macros with-http-response y with-http-body son parte de AllegroServe. El primero inicia el proceso para generar una respuesta HTTP y puede ser usado, como en este caso, para especificar cosas como el tipo de contenido que será devuelto. El with-http-body actualmente envía la cabeceras HTTP y luego ejecuta su cuerpo, que debería contener el código que genera el contenido de la respuesta. Dentro de with-http-response pero antes de whit-http-body, puedes agregar o cambiar las cabeceras HTTP para enviarlas en la respuesta. La función request-reply-stream es también parte de AllegoServe y devuelve el stream que se escribirá como salida para ser enviada al navegador. Como lo muestra esta función, puedes usar solo FORMAT para imprimir el HTML al stream devuelto por request-reply-stream.
En la próxima sección, se mostrara la manera mas conveniente para generarla por programa.
NOTA: El ~@ seguido por una nueva linea le dice a FORMAT que ignore los espacios en blanco después de la nueva linea, lo que te permite indentar tu código en forma amigable sin necesidad de agregar un grupo de espacios al HTML. Los espacios en blanco no significan nada en HTML, y no les interesan al navegador, pero esto hace que el código HTML generado luzca un poco mas amigable a las personas. Ahora estas listo para publicar esta función:

(publish :path "/numero-aleatorio.html" :function 'numero-aleatorio)
#<COMPUTED-ENTITY @ #x7262bab2>

Así como lo hace la función publish-file, el argumento :path especifica el path de la URL de esta función al ser invocada. El argumento :function especifica tanto el nombre como el objeto de la función actual. Usando el nombre de una función, como se muestra aquí, te permite redefinir la función sin tener que volver a publicarla y AllegroServe usará la nueva definición de la función. Después de evaluar la llamada a publish, puedes escribir la dirección en tu navegador http://localhost:2001/numero-aleatorio.html para obtener una página con un numero aleatorio en ella, como muestra la figura.