martes, 5 de febrero de 2008

conversaciones con PSIs

Intento sacar un buen sistema de conversación con PSIs, para lo cual el sistema de lectura de objetos de Inform me resulta fatal. Lo suyo sería implementar un modo de parseado alternativo dentro del propio parser, un sistema PAWSlike de reconocimiento de cadenas de texto, de chorizos de texto no acotados necesariamente por espacios ni por palabras completas.

Mi antiguo parser en C, DISAC funcionaba así:
Tenía 4 tablas de vocabulario: Verbos, Nombres, Objetos y Adjetivos, teniendo Verbos la obligación de ser la primera cadena de la orden, y Objetos estando en principio reservada para objetos cogibles.
El parser buscaba coincidencias letra a letra sin importarle los caracteres que sobraran: signos de puntuación, espacios, sufijos, declinaciones, palabras extra... Cuando los espacios fueran importantes, sobre todo en palabras cortas que pudieran estar contenidas en otras, el espacio iba en la propia definición de vocabulario de esa palabra, con un cuadradito, tal que '#asi#'
De modo que era vocabulario válido cosas como:
'bebe', 'di#', 'por#que', 'te#llama', 'destruy', 'llave#roja', '#con#', '#en#'...

No hacía falta ni introducir las palabras completas ni una a una: las "palabras" eran a veces cachos o trozos de frase.

A la hora de buscar coincidencias, se jugaba con cuatro posibles valores de cada tabla:

1- Vocabulario concreto encontrado.
2- Indiferente de si ha habido coincidencia.
3- No debe haber habido coincidencia.
4- Debe haber habido alguna palabra de esa lista detectada, da igual la que sea.

De cara a las conversaciones, esto permitía jugar con términos y trozos de frase por los que existiera una alta probabilidad de que el jugador tuviera que pasar para formular una pregunta concreta.
Por ejemplo, para detectar una pregunta de ¿cómo te llamas?, la mayor parte de los casos con tuteo se podrían cazar definiendo dos cadenas:

'te#llama' -> Cómo te llamas, cómo te llaman.
'tu#nombre' -> cúal es tu nombre, dime tu nombre, quiero saber tu nombre.

Teniendo en cuenta los posibles errores:
-te llaman para cenar, no quiero saber cómo te llamas, tu nombre es muy feo.

Pero prefiero que se conceda el beneficio de la duda.


Volvamos a Inform, aquí existe la librería Etemas que permite jugar con UN objeto con tres modos distintos de parsing:
-parsing estricto: Deben aparecer todas las palabras de vocabulario del objeto-tema
-parsing normal: Pueden aparecer algunas, aunque falla si metes palabras no reconocidas delante.
-parsing flexible: Con que aparezca una vale, las palabras desconocidas se ignoran.

El problema es que las palabras deben estar enteras y no contener espacios, cuando ya he explicado por qué creo más útil un vocabulario definido a trozos e incluso incompleto: 'por#que' 'donde#est' ...

Descartando una inteligencia artificial capaz de reconocer las gramáticas, cosa que hace el parser pero en un argot limitado a órdenes, veo más viable basarse en trozos. Como cuando estamos rodeados de ruido y tan sólo escuchamos fragmentos de las palabras de nuestro interlocutor, y a partir de trozos clave -y del contexto- tratamos de recomponer el mensaje completo.

...

He hecho un apaño en Etemas para poder jugar con cinco listas de vocabulario independientes por objeto, y barajo incluir más de un objeto en el parsing para poder tener 5+5+5... de forma independiente.
Dos o tres sectores de vocabulario: uno para órdenes y encabezados de preguntas (averiguar si el jugador quiere saber dónde, cuándo, por qué, o si ordena que se vaya, que se quede, que vaya con él, que le dé algo); y el otro para saber el sitio, el objeto... etc.
El orden en el que se escriban dará igual, los diversos objetos se sacarán del mismo texto barriéndolo repetidamente, será lo mismo escribir: di a pepe dame la manzana, que di a pepe la manzana dame

Para eso, se añade a Etemas un cuarto modo de parsing: parsing_multiple, basado en el parsing_flexible:

[ FlexIntnombreMultiple obj u i w n;
for ( i = 0 : i < consult_words : i++ ) {
w=NextWordStopped();
if( u== 1 )if (WordInProperty (w, obj, name)) n++;
if( u== 2 )if (WordInProperty (w, obj, name_f)) n++;
if( u== 3 )if (WordInProperty (w, obj, name_mp)) n++;
if( u== 4 )if (WordInProperty (w, obj, name_fp)) n++;
if( u== 5 )if (WordInProperty (w, obj, adjectives)) n++; }
return n; ];


Ésta función será llamada con dos parámetros, el objeto obj, y un valor 'u' que indica qué lista de vocabulario de ese objeto se consultará. En una primera fase se consultarán todas y se sumarán todos los valores para descubrir el objeto-tema ganador... excepto la lista de adjetivos, cuya puntuación no será tenida en cuenta, y si aparece en la función es porque sí que será necesaria para la segunda fase. Se puede ver en AveriguarTema cómo se realiza la suma disgregada como si se tratara de un objeto-tema normal con una sola lista name.

[ AveriguarTema T i n k max tmax nn;
max=0; ! La mejor puntuación de momento
tmax=0; ! El objeto correspondiente a esta puntuación
k=wn;
objectloop (i in T) {

!...etc

if (i has parsing_multiple){
wn=consult_from; nn=0;n=0;
nn=FlexIntnombreMultiple (i,1);
n=n+nn;wn=consult_from;
nn=FlexIntnombreMultiple (i,2);
n=n+nn;wn=consult_from;
nn=FlexIntnombreMultiple (i,3);
n=n+nn;wn=consult_from;
nn=FlexIntnombreMultiple (i,4);
n=n+nn;wn=consult_from;
! nn=FlexIntnombreMultiple (i,5);
! n=n+nn; !adjectives no suma de cara a la palabra ganadora
}
!...


Y ahora la fase 2, justo antes de devolver el objeto ganador, analizamos nuevamente ese objeto, pero ésta vez dejando constancia de qué listas de ese objeto contienen vocabulario escrito por el jugador y cuales no:

!... if(tmax>0){
i=1;
wn=consult_from; !name
if(FlexIntnombreMultiple(tmax,1)>0)i=i*2;
wn=consult_from; !name_f
if(FlexIntnombreMultiple(tmax,2)>0)i=i*3;
wn=consult_from; !name_mp
if(FlexIntnombreMultiple(tmax,3)>0)i=i*5;
wn=consult_from; !name_fp
if(FlexIntnombreMultiple(tmax,4)>0)i=i*7;
wn=consult_from; !adjectives
if(FlexIntnombreMultiple(tmax,5)>0)i=i*11;
}
switch (T){
temas:temas.capciones=i;
temas2:temas2.capciones=i;
!temas3:temas3.capciones=i; !...etc
}
return(tmax); ! Y retornar el mejor hallado
! Observar que no hay intento de aclarar
! ambigüedades si dos o más coincidieran en
! puntuación
];


Usamos números primos para otorgar el valor final de la variable(propiedad) temas.capciones.
Posteriormente, podremos jugar con ese valor, por ejemplo:
Si temas.capciones vale 2, significa que sólo ha habido coincidencia en la lista de name.
Si temas.capciones vale 66, significa que ha habido coincidencias en la listas de name, name_f y adjectives.

Aquí un ejemplo de un tema, con sus valores de consulta:

xTema tSaludos "" Temas
with name 'hola' 'saludos' 'tal' 'como',
name_f 'dias' 'noches' 'tardes',
name_mp 'adios' 'voy' 'chao' 'adios' 'luego' 'vista' 'otra' 'vemos',
name_fp 'haces' 'haciendo' 'pasa' 'ocurre' 'pasado' 'ocurrido',
adjectives 'que' 'estas' 'hasta' 'buenas' 'buenos',
;
! (i==2) !hola
! (i==22) !qué tal estás
! (i==5 || i==15) !Adios
! (i==77) !¿Qué haces?
! (i==33) !Buenos días, tardes, noches


Es conveniente al menos una segunda vuelta, para detectar otro objeto de la misma forma, y poder elaborar estructuras más complejas con objetos INDEPENDIENTES. Con una sola vuelta, aunque cada objeto tenga 5 listas, éstas dependen de estar contenidas en el mismo objeto.

Ejemplo de objeto de primera vuelta:

xTema tIR "" Temas1
with name 'vete' 've' 'ponte' 'colocate' 'situate' 'subete' 'sube' 'dirigete' 'largate' 'marchate' 'metete' 'entra' 'pirate' 'sal',
name_f 'vente' 'ven' 'acompaname',
name_mp 'espera' 'esperame' 'quedate' 'aguarda',
!name_fp 'xxx',
!adjectives 'xxx',
;
!(i%2==0) !vete, ve a...
!(i%3==0) !ven, acompañame
!(i%5==0) !espera
!(i%7==0) ! sin uso aún
!(i%11==0) ! sin uso aún


Ejemplo de objeto de segunda vuelta:

xTema tsitios1 "" Temas2
with name 'rio',
name_f 'camino',
name_mp 'casa',
name_fp 'norte',
adjectives 'sur',
;
!(i%2==0) -> en algún río
!(i==6) -> en/a el camino del río
!(i==105) -> en/a el camino de la casa de pepe
!(i==35) -> en/a la casa del norte
!(i==55) -> en/a la casa del sur.
!(i%2==0) -> en algún río
!(i%35==0) -> en la casa del norte, o en el camino de la casa del norte, o al sur de la casa del norte, o en el río de la casa del norte
!...etc

Los lugares que no quepan aquí se meten en otro tema del mismo objeto Temas y listo.
Se podría crear una tercera vuelta de consulta, una cuarta... todos los objetos que se quieran a costa de velocidad de parseado, simplemente llamando repetidamente a AveriguarTemas(), con objetos contenedores de temas independientes, y previendo en el código el almacenamiento en variables diferentes del objeto ganador de cada ronda, así como sus capciones, para luego poder consultarlos en la función de conversación.

Pongamos que tenemos tres objetos contenedores de temas:
Temas1, Temas2, Temas3
Cuyos objetos ganadores se almacenan en las variable: To1, To2 y To3
Y las capciones en: Cap1, Cap2, Cap3
Podríamos tener un código de conversación tal que:

[xxxhablarSub tema i;
print "", (name)noun, " ";
print "responde: ";
to1=AveriguarTema(Temas1);
to2=AveriguarTema(Temas2);
to3=AveriguarTema(Temas3);

if(to1==tIR){!a
if(to2==tsitios1){!a1
!entiende "espera" y "casa" y "norte"
if(cap1%5==0 && cap2==35)"Me estás diciendo que espere en la casa del norte.";
}!a1
!entiende "ve" y ningún sitio de la lista, luego suponemos un ¡márchate!
if(cap1%2==0 && to2==0)"Me estás diciendo que me vaya.";
}!a
if(to2==tsitios1){ if (cap2%3==0)"Quieres saber algo referente un camino ¿eh?";}
!...etc

Otra corrección es unificar el sistema de conversación, que en Inform es disparatado, estando dividido en decir, preguntar sobre, ordenar, pedir...

Mi sistema ideal es: PERSONAJE, MENSAJE
aunque de momento lo he unificado como: DI A PERSONAJE MENSAJE

4 comentarios:

Mel Hython dijo...

Jarel, me parece que lo que propones resulta demasiado complicado de usar.

¿Has echado un vistazo a la aproximación de attoPAWS? Creo que se podría ganar en simplificación si se define un vocabulario explícito con verbos, nombres, preposiciones, etc...

De todas formas, sin irse a ese extremo podrías cocinar un poco más la aproximación, si además del 'objeto tabla' que creas para hacer el análisis crear otro 'objeto tabla' para hacer el macheo e inventas una sintaxis 'siguiendo' lo que hizo Zak en attoPAWS. Así tendría 'al final' una gramática flexible nueva con una sección de vocabulario y otra sección de pattern matching separada.

Herel dijo...

Sí, el problema es que lo que me he ahorrado en código de modificación de la librería Etemas, lo pago luego con creces a la hora de programar todas las respuestas de la conversación.

Me voy a mirar mejor el parsing de Attopaws cuando tenga tiempo, a primera vista parece justo lo que buscaba.

Alberto Vilches dijo...

Hola! Parece un trabajo bastante elaborado, enhorabuena. Solo quería comentarte que si en vez de números primos, usas potencias de dos (1,2,4,8,16,32) la suma de los números es siempre distinta también (son las posiciones de los bits en un byte). Un saludo!

Herel dijo...

La verdad es que sí, además así me llega con una variable -que en Inform tienen valores entre -10.000 y 10.000- para muchas más capciones. Se podría decir que he hecho el "primo" :D