El algoritmo, que permite que Eliza genere una respuesta acorde a las consultas del usuario, se basa en comparar término a término la data ingresada versus una lista de templates con las respuestas predefinidas. Las ideas iniciales para desarrollar este algoritmo fueron obtenidas de [2] y de [3], a partir de estas fuentes se realizó una implementación propia.
El formato de cada template es el siguiente:
template([lista con las palabras del estímulo], [lista con las palabras de la respuesta de Eliza], [lista con las posiciones de las palabras clave en el estímulo empezando de cero]).
Un template de ejemplo tendrá la forma:
Las palabras clave le permiten a Eliza generar una
coincidencia para diferentes listas de palabras, tales como “i am sad.” o “i am
angry.”. Las palabras claves se reemplazarán en la respuesta en las posiciones
donde hay números y no palabras.
La función que realiza la comparación es la función “match”.
En el estímulo, las palabras clave están marcadas por predicados, mientras que
el resto de los términos son atómicos. Cuando la función match detecta un dato
no atómico, usando la función de SWI-Prolog “atom”, lo ignora y continúa con la
comparación término a término entre la data ingresada y el estímulo. Si se
encuentra un template que coincida, devolverá la respuesta correspondiente y la
lista de índices. Los índices deben coincidir con las posiciones de los datos no
atómicos en el estímulo del template.
La función “replace0” se encarga de reemplazar las palabras claves de la data ingresada en las posiciones donde la respuesta del template tiene números. Las palabras clave pueden ser una o más. Para saber qué palabra clave corresponde a qué posición en la respuesta, los números se van incrementando de uno en uno. Si no hay palabras para reemplazar, la lista de índices será una lista vacía.
Esta función llama a las siguientes funciones ya implementadas en SWI-Porlog:
nth0: devuelve el elemento de una lista en una posición dada.
select: reemplaza el valor de un elemento de una lista por otro en función a sus valores.
append: concatena dos listas. En este caso va concatenando
las palabras de la respuesta, junto con la palabra reemplazada en la posición
donde hay un número, palabra por palabra.
La función “replace0” también genera las respuestas a
preguntas y consultas del tipo “do you like ...?”, “you are ...” y “do you (verb)
...?”. Los templates correspondientes tienen el siguiente formato:
Si la función “replace0” detecta una de las banderas (flags) configuradas, buscará la respuesta correspondiente en las funciones elizaDoes, elizaIs y ElizaLikes, las cuales evalúan los hechos que determinan si Eliza hace, es o le gusta algo.
El código donde se declaran estos hechos es el siguiente:
El código que le permite al usuario ingresar sus consultas es el siguiente:
La función “readln” convierte las consultas del usuario en
una lista. Luego el programa escogerá el primer template de la base del
conocimiento, a continuación el estímulo de ese template, y la consulta del
usuario (Input), son ingresados a la función “match”. Si el estímulo no coincide
con la consulta, Prolog empezará el backtracking, escogiendo el siguiente
template y repitiendo la operación. El último template de la base de
conocimiento hace que la función “match” retorne verdadero si ninguno de los
otros estímulos de los otros templates coincide con la consulta del usuario.
Esto evita que se rompa el bucle de Eliza y finalice el programa de forma
abrupta. A continuación se llama a la función “replace0”, para luego imprimir la
respuesta en la pantalla. Eliza seguirá esperando las consultas del usuario
mientras éste no ingrese “bye” o “bye.”.
II
Conversando con Eliza
Para interactuar con el programa Eliza, se debe ejecutar el intérprete SWI-Prolog,
abrir el archvo eliza_complete.pl y compilarlo. En la consola de SWI-Prolog se
deberá ingresar “eliza.” Eliza mostrará un mensaje de bienvenida y esperará por
más consultas del usuario. Éstas deben estar en minúsculas y tener un punto al
final.
Una conversación de ejemplo es la siguiente:
Eliza da las respuestas en forma de listas de palabras, separadas por comas y entre corchetes. Se mantuvo este formato porque en una aplicación Eliza real, es muy probable que las respuesta halladas requieran un procesamiento posterior.
La aplicación ha sido probada en SWI-Prolog versión 7.2.1. No se ha probado en versiones anteriores. No funcionará en versiones de SWI-Prolog que no tengan implementadas las funciones length, nth0, atom y select.
Esta implementación de Eliza no tiene memoria, es decir, no recuerda las
consultas ingresadas y sus respuestas, por lo que la coherencia de la
conversación depende del usuario. Por ejemplo, para Eliza es válido ingresar
primero la consulta “because you are creepy.” Y luego “i hate you.”.
Para ampliar las respuestas de Eliza a más estímulos basta crear más templates con los formatos ya mencionados.
En este programa, la aparente inteligencia de Eliza está en función a la
amplitud de su base de conocimientos. Como la coincidencia de las consultas del
usuario y los estímulos se realiza término a término, la base de conocimiento de
Eliza debe ser bastante grande para producir una conversación coherente, y poder
respetar género, número, tiempos verbales, etc. Un programa más completo
consideraría toda una lista de palabras clave en lugar de sólo una.
El código completo es (Escrito en Prolog y probado en Swi Prolog v7.2.):
% by: Y3l1nna_Broken_Window: % la función match permite crear un output para varios inputs. eliza:- writeln('Hi, I am Eliza your chatbot, please enter your query, use only lowercases and a dot at the end:'), readln(Input), eliza(Input),!. eliza(Input):- Input == ['bye'], writeln('Goodbye. I hope I have helped you.'), !. eliza(Input):- Input == ['bye', '.'], writeln('Goodbye. I hope I have helped you.'), !. eliza(Input) :- template(Stim, Resp, IndStim), match(Stim, Input), % si he llegado aquí es que he % hallado el template correcto: replace0(IndStim, Input, 0, Resp, R), writeln(R), readln(Input1), eliza(Input1), !. % algunos saludos % debido al backtracking, los templates más específicos deben ir antes % de los más genéricos. Eso se nota acá: template([hi, my, name, is, s(_), '.'], ['Hi', 0, 'How', are, you, '?'], [4]). template([hello, my, name, is, s(_), '.'], ['Hello', 'How', are, you, 0, '?'], [4]). template([hi, ',', my, name, is, s(_), '.'], ['Hi', 0, 'How', are, you, '?'], [5]). template([hello, ',', my, name, is, s(_), '.'], ['Hello', 'How', are, you, 0, '?'], [5]). template([hi, _], ['Hi', 'How', are, you, '?'], []). template([hello, _], ['Hello', 'How', are, you, '?'], []). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% template([i, s(_), i, am, s(_),'.'], [why, do, you, 0, you, are, 1, '?'], [1, 4]). template([i, s(_), you, '.'], [why, do, you, 0, me ,'?'], [1]). template([i, am, s(_),'.'], [why, are, you, 0, '?'], [2]). % pregunta algo que le gusta a eliza template([do, you, like, s(_), _], [flagLike], [3]). % pregunta algo que hace eliza template([do, you, s(_), _], [flagDo], [2]). % pregunta algo que es eliza template([you, are, s(_)], [flagIs], [2]). template([are, you, s(_), '?'], [flagIs], [2]). template([how, are, you, '?'], [i, am, fine, ',', thanks, for, asking, '.'], []). template([i, think, _], [well, that, is, just, your, opinion], []). template([because, _], [that, is, not, a, good, reason, '.'], []). template([i, have, s(_), with, s(_), '.'], ['You', have, to, deal, with, your, 0, and, your, 1, in, a, mature, way, '.'], [2, 4]). template([i, s(_), _], [i, can, recommend, you, a, book, about, that, issue], []). template([please, s(_), _], ['No', i, can, not, help, ',', i, am, just, a, machine], []). % didn't passed the Turing Test template([tell, me, a, s(_), _], ['No', i, can, not, ',', i, am, bad, at, that], []). % classic AI problem: una máquina aún no puede contar una historia coherente. template(_, ['Please', explain, a, little, more, '.'], []). % cuando se ingresa algo que Eliza no tiene en sus templates, esto debe ir al final o eliza responderá a todo con esto. % Lo que le gusta a eliza : flagLike elizaLikes(X, R):- likes(X), R = ['Yeah', i, like, X]. elizaLikes(X, R):- \+likes(X), R = ['Nope', i, do, not, like, X]. likes(apples). likes(ponies). likes(zombies). %%%%%%%%%%%%%%%%%%%%%% % lo que hace eliza: flagDo elizaDoes(X, R):- does(X), R = ['Yes', i, X, and, i, love, it]. elizaDoes(X, R):- \+does(X), R = ['No', i, do, not, X ,'.', it, is, too, hard, for, me]. does(study). does(cook). does(work). %%%%%%%%%%%%%%%%% % lo que es eliza: flagIs elizaIs(X, R):- is0(X), R = ['Yes', i, am, X]. elizaIs(X, R):- \+is0(X), R = ['No', i, am, not, X]. is0(dumb). is0(weird). is0(nice). is0(fine). is0(happy). is0(redundant). % http://stackoverflow.com/questions/12939425/prolog-access-specific-member-of-list % magic happens here :D % lo que hace match es comparar el input y el estímulo término a % término, ignorando los s(X) en cada template. Si match falla en el % loop de la función eliza, empezará el backtracking que permite % recorrer los templates ingresados. match devolverá verdadero si haya % un estímulo (primer término del template) que coincide con el Input, y % devolverá la respuesta. match([],[]). match([], _):- true. match([S|Stim],[I|Input]) :- atom(S), % si I es un s(X) devuelve falso S == I, match(Stim, Input),!. match([S|Stim],[_|Input]) :- % I es un s(X), lo ignoro y continúo con el resto de la lista \+atom(S), match(Stim, Input),!. % esta función es para que se muestre bien en pantalla las respuestas de % eliza. Además responde a lo que le gusta Eliza y a lo que hace, la % función select reemplaza el número en la respuesta por la palabra % clave hallada: replace0([], _, _, Resp, R):- append(Resp, [], R),!. % Eliza likes: replace0([I|_], Input, _, Resp, R):- nth0(I, Input, Atom), nth0(0, Resp, X), X == flagLike, elizaLikes(Atom, R). % Eliza does: replace0([I|_], Input, _, Resp, R):- nth0(I, Input, Atom), nth0(0, Resp, X), X == flagDo, elizaDoes(Atom, R). % Eliza is: replace0([I|_], Input, _, Resp, R):- nth0(I, Input, Atom), nth0(0, Resp, X), X == flagIs, elizaIs(Atom, R). replace0([I|Index], Input, N, Resp, R):- length(Index, M), M =:= 0, nth0(I, Input, Atom), select(N, Resp, Atom, R1), append(R1, [], R),!. replace0([I|Index], Input, N, Resp, R):- nth0(I, Input, Atom), length(Index, M), M > 0, select(N, Resp, Atom, R1), N1 is N + 1, replace0(Index, Input, N1, R1, R),!.