ONTOLOGIE OWL IN DALI

Tavola dei contenuti

1 Introduzione

1.1 Prerequisiti

Questo documento assume che DALI sia correttamente installato sul vostro PC ( se così non è, andate qui ), e che il lettore sappia cosa sia e come funzioni un interprete DALI. Per poter comprendere questo documento occorre conoscere ( almeno concettualmente ) cos'è un'ontologia OWL ( definita nella sintassi RDF/XML ) e come puo' essere utilizzata. Nel caso non si disponga di queste conoscenze, le conoscenze sull'OWL sono facilmente acquisibili consultando i seguenti:

Owl 2 Language Overview
Owl 2 Language Primer

e per approfondire si puo' anche consultare

Owl 2 Structural Specification and Functional Syntax
Owl 2 Quick Reference

Per quanto riguarda lo SPARQL invece si consigliano

Sparql Query Language
Turtle Terse RDF Triple Language

Tuttavia si consigliano questi link solo se si ha una conoscenza almeno concettuale di cosa sia e su quali principi si basi il formato RDF. Nel caso non si abbiano queste conoscenze, è opportuno anzitutto consultare:

RDF Primer

1.2 Cosa occorre installare

Una volta che ci si è procurati un'ontologia OWL, abbiamo bisogno di un repository semantico che ci permetta di interrogarla mediante il linguaggio di query SPARQL e di installare un qualche reasoner. A questo scopo bisogna procurarsi un framework che permetta di creare, eliminare e gestire questi repositiry semantici. SESAME è un framework open-source che permette la gestione di repository semantici. Potete procurarvi l'ultima versione di SESAME ( 2.3.0 all'atto di stesura di questo documento ) qui. Poichè SESAME è un Servlet JAVA, avrete bisogno di Tomcat. Potete scaricare Tomcat da qui.

1.3 Architettura DALI + SESAME

L'idea che è alla base dell'interazione tra DALI e SESAME è illustrata in figura:

L'agente DALI ( da intendersi tutt'uno con il modulo per il metaragionamento ) a seconda delle necessità ( magari al fine di interpretare un predicato di cui non conosce il significato ) manda una richiesta alla libreria di comunicazione con le ontologie OWL. Questa libreria, avvalendosi dello "SPARQL TEMPLATE FILE" ( file contenente dei templates di query SPARQL e che puo' essere liberamente modificato dall'utente ), si occupa di tradurre la specifica richiesta in una richiesta HTTP comprensibile da Tomcat ( utilizzato in figura come webserver ). Il Servlet SESAME, che gira su Tomcat, ragionerà su questa richiesta ( la quale non è altro che una query SPARQL composta dalla libreria di comunicazione utilizzando lo SPARQL TEMPLATE FILE ) e restituirà i risultati sotto forma di documento XML ( secondo il formato definito in SPARQL Query Results XML Format ) alla libreria di comunicazione. Questa tradurrà i risultati in un formato che sia comprensibile all'agente DALI, il quale ragionerà sul risultato ottenuto e si comporterà di conseguenza.

2 Limitazioni e Convenzioni

2.1 Mapping tra concetti nell'ontologia e concetti nella KB

Un agente DALI riceve notizie dal mondo esterno, e da altri agenti, attraverso i messaggi. I messaggi ricevuti possono innescare nell'agente diversi tipi di comportamenti: una reazione; una verifica sulla propria base di conoscenza; un'azione di modifica sulla propria KB ecc. Abbiamo parlato di messaggi, ma non abbiamo ancora specificato cos'è un messaggio. Un messaggio è un predicato PROLOG con o senza argomenti. Un predicato è formato da un Atomo seguito o no da un certo numero di Argomenti ( Termini ) racchiusi tra parentesi tonde e separati da una virgola. Gli Atomi sono delle stringhe alfanumeriche costanti che incominciano in lower_case ( escluderemo in questa trattazione gli Atomi del tipo "'parola'" e gli Atomi speciali quali ":-" ); danno il nome ad un oggetto specifico o ad una relazione specifica, cioè rappresentano in maniera testuale i concetti della realtà. Ma come può un agente sfruttare un'ontologia al fine di interpretare un messaggio in ingresso? Possiamo fare un parallelo con la comunicazione umana. Supponiamo di dover tradurre una frase dall'inglese, e di avere a nostra disposizione un dizionario Inglese-Italiano ed un dizionario di Italiano. La prima cosa necessaria è che le parole della frase siano rintracciabili all'interno del dizionario Inglese, ossia deve esistere un mapping tra le parole della frase ed alcune parole del dizionario. Una volta tradotta la parola, potremmo ancora non riuscire a capire la semantica della stessa. Può a questo punto aiutarci il dizionario di Italiano, fornendoci la semantica della parola che abbiamo appena tradotto. L'ultima ( ma non ultima ) cosa che dobbiamo fare è ricavare la Struttura della frase inglese, ossia capire se la parola che abbiamo cercato è un "verbo" oppure un "sostantivo", poiché se la parola è un verbo allora va' coniugata, se è un sostantivo magari va resa al singolare o al plurale eccetera..
Torniamo adesso al problema dell'agente che deve interpretare un messaggio ( predicato prolog ) in ingresso. In questo caso le "parole" che compaiono all'interno del messaggio sono gli Atomi che compaiono nello stesso, ossia il funtore del predicato ed, eventualmente, qualche argomento. Così come nel caso del dizionario Inglese, per poter consultare l'ontologia abbiamo bisogno di un mapping tra le "parole" del messaggio ( Atomi ) ed i Concetti dell'ontologia ( URI o Letterali ). Nei prossimi paragrafi approfondiremo i dettagli di questo mapping.

2.2 Limitazioni sugli URI

Poichè OWL è un linguaggio nato per il Web Semantico, le risorse vengono indicate in RDF ( e di conseguenza, in OWL ) attraverso gli Uniform Resource Identifiers ( RFC 3986 ), al fine di fornire ad ogni risorsa un identificatore che sia univoco a livello mondiale.
Questo crea un problema, poichè gli atomi prolog possono essere espressi solo mediante caratteri alfanumerici ( mentre invece un URI può contenere parecchi simboli speciali ).
Qualche esempio:

Appare evidente quindi che gli URI non possono essere usati per indicare una risorsa PROLOG, poichè le risorse in PROLOG vengono indicate mediante gli atomi.
Occorre fare un'altra considerazione. La sintassi degli URI, infatti, puo' variare ( e di molto ) da un URI all'altro, in quanto ogni URI ha una propria componente detta "Scheme Name" che definisce la sintassi per quello specifico URI. A titolo di esempio, basta dare un'occhiata alla manciata di URI qui di seguito:

ftp://ftp.is.co.za/rfc/rfc1808.txt
http://www.ietf.org/rfc/rfc2396.txt
ldap://[2001:db8::7]/c=GB?objectClass?one
mailto:John.Doe@example.com
news:comp.infosystems.www.servers.unix
tel:+1-816-555-1212
telnet://192.0.2.16:80/
urn:oasis:names:specification:docbook:dtd:xml:4.1.2

Fare in modo che DALI possa interpretare correttamente qualunque URI possibile non è compito semplice. Considerando poi che la stragrande maggioranza degli URI utilizzati nelle ontologie OWL sono in realtà degli URL ( peraltro con un pattern ben definito ), ho deciso di restringere l'intervallo di URI interpretabili come "Risorsa" da parte di un'agente DALI agli URL ( privati della parte "querystring" e che non denotano un indirizzo E-MAIL ).
Cio' significa, che gli URI interpretabili come risorsa prolog ( nell'implementazione attuale ) sono solo:

ftp://ftp.is.co.za/rfc/rfc1808.txt
http://www.ietf.org/rfc/rfc2396.txt

Questa decisione, se da un lato restringe il numero di ontologie utilizzabili da un agente DALI, dall'altro nulla toglie al potere ed all'espressività dell' RDF, e quindi di OWL. E comunque non è una decisione immodificabile. In un lavoro futuro, si potrà scegliere di fare a meno di questa limitazione e di considerare gli URI così come sono, eliminando di fatto le limitazioni che questo approccio comporta. Dei possibili sviluppi futuri di questo lavoro si parlerà nel Capitolo 6 di questo documento.

2.3 Come DALI interpreta un URI

Nel paragrafo precedente abbiamo accennato al fatto che c'è differenza tra il SET di caratteri che denota un URI, ed il SET di caratteri che denota un atomo PROLOG. La restrizione che abbiamo effettuato sugli URI, se da un lato ci permette una più facile gestione degli stessi ( in quanto hanno tutti un pattern fisso ), dall'altro non ha certamente risolto il problema. Prendiamo un'ontologia a caso ( l'ontologia dell'esempio è consultabile qui ) ed esaminiamo come è composto un URI:

è comodo ( per le nostre finalità ) considerare un URI come un'entità composta di due parti: Un'idea potrebbe essere quella di far coincidere la parte NOME di un URI con un atomo PROLOG. Analizziamo un altro caso:

In questo secondo URI la parte "Nome" è un anchor_id del file "beer.rdf".

Fatte queste premesse, possiamo ora definire un mapping tra atomi prolog ed URI.

Definizione 1 : Mapping atomi prolog - URI

Un URI è traducibile in un atomo PROLOG ( secondo l'implementazione attuale dell' interprete ) in due casi:

e la parte "Nome" NORMALIZZATA è un atomo PROLOG.

Viceversa un ATOMO PROLOG DENORMALIZZATO può corrispondere alla parte NOME di una risorsa identificata con un URI.

Definizione 1.1 : Stringa Normalizzata

Data una stringa di A di caratteri alfanumerici (al più che termina con il simbolo di backslash "/") e che non incomincia con un numero, definiamo la sua versione NORMALIZZATA N come:

Definizione 1.2 : Atomo Denormalizzato

Dato un atomo prolog A, definiamo la sua versione denormalizzata D come una stringa uguale ad A eccetto che per i caratteri di underscore "_", che in D vengono indicati come simboli di blank " ".


Abbiamo così definito un mapping tra i nomi OWL ed i nomi degli atomi PROLOG, e viceversa. Ciò significa che adesso possiamo indicare risorse URI mediante atomi prolog, e che viceversa un qualunque atomo prolog può corrispondere ad un singolo URI ( anche se in verità il "viceversa" è vero solo nelle ontologie dove gli URI che indicano concetti differenti presentano parti Nome differenti ).

Esempio 1

Si consideri la seguente tripla in RDF:

http://my_ontology/#Pilsner/ rdfs:subClassOf http://my_ontology/#Beer/.

e di avere un agente "agente 1" nella cui KB sia asserito il seguente atomo:

i_like(pilsner).

Allora alla domanda di un altro agente "agente 2"

do_you_like(beer).

il primo agente eseguirà i seguenti passi: Poichè nel nostro esempio c'è una corrispondenza, l'agente 1 risponderà:

yes_i_like.

2.4 Come DALI interpreta un Letterale

Nel paragrafo precedente abbiamo introdotto un mapping tra le risorse OWL ( URI ) e le risorse PROLOG ( atomi ). Ciò rende possibile far coincidere alcuni termini della base di conoscenza dell'agente con risorse dell'ontologia, cosicchè l'agente possa ragionare sulla semantica degli stessi.

Ma nell'OWL non esistono solo le risorse, bensì esistono anche delle proprietà che possono essere definite su quelle risorse. Una proprietà è anche rappresentabile come una coppia <attributo, valore>, ed i valori potrebbero non essere degli URI, ma anche dei Letterali. È necessario quindi stabilire un mapping tra gli atomi prolog ed i Letterali OWL.

Definizione 2 : Mapping atomi prolog - Letterali

Un Letterale OWL è traducibile in un atomo PROLOG che corrisponde alla versione NORMALIZZATA del Letterale.

Viceversa, un atomo prolog DENORMALIZZATO può corrispondere ad un Letterale di un'ontologia OWL.

2.5 Il problema dell'univocità

Il mapping diretto "atomo PROLOG" - "risorse OWL" che abbiamo introdotto al paragrafo precedente presenta un problema: un URI garantisce l'univocità di una determinata risorsa, cosa che la sola parte NOME non può fare. Il programmatore DALI deve tenerne conto, ed al fine di garantire l'univocità delle risorse deve utilizzare ontologie dove vale la seguente proprietà:

Se due URI hanno la stessa parte Nome, allora identificano la stessa risorsa.

Non è sufficiente che questa proprietà valga solo all'interno di una singola ontologia, ma dovrebbe valere anche TRA le ontologie coinvolte durante una comunicazione tra agenti. Quest'ultimo aspetto verrà approfondito con l' esempio dei tre agenti.

2.6 Il problema della coerenza

Ogni agente ha la propria KB scritta in prolog. Però, con l'introduzione delle basi di conoscenza OWL, gli agenti si trovano a possedere due basi di conoscenza separate ( dove ci sono alcuni atomi che corrispondono a risorse presenti nell'ontologia, mentre altri no ). Il programmatore DALI quindi si deve preoccupare della coerenza delle informazioni presenti nelle due diverse basi di conoscenza, al fine di evitare pericolose situazioni di ambiguità.

2.7 Il problema dei Tre Agenti

Supponiamo di avere tre agenti A, B, C, in cui :

In uno schema dove A può apprendere nuove ontologie, se ad A fosse permesso di apprendere, dopo due comunicazioni separate con C e con B, le ontologie di C e di B ( che sono tra di loro incompatibili ), allora l'ontologia estesa di A sarebbe inconsistente.

Esempio 2

Supponiamo che questa sia l'ontologia dell'agente B:

prefixB:Scarpa rdf:type prefixB:Person

e supponiamo che questa sia l'ontologia dell'agente C:

prefixC:Scarpa rdf:type prefixC:Wear

Allora dopo una comunicazione con B e con C, l'ontologia di A avrebbe la forma:

prefixB:Scarpa rdf:type prefixB:Person
prefixC:Scarpa rdf:type prefixC:Wear

Poichè le due risorse ( differenti ) prefixB:Scarpa e prefixC:Scarpa hanno la stessa parte Nome, allora la proprietà di univocità è violata.
Per superare questo problema, quando un'agente assorbe l'ontologia di un'altro agente, la usa solo se ne ha bisogno per parlare con quell'agente. Altrimenti, utilizza solo la propria. Questo piccolo accorgimento permette di superare il problema dei tre agenti, ed è utilizzato nel metaragionamento DALI.

2.8 Mondo Aperto vs Mondo Chiuso

Nel prolog vale l'assunzione di mondo chiuso, in quanto se non è possibile derivare il valore di verità di un predicato all'interno di una base di conoscenza KB, allora il test fallisce ed il prolog risponde che quel predicato è falso. Ciò non è vero in OWL, dove vale l'assunzione di mondo aperto. Ma le due cose non sono del tutto inconciliabili. Riprendiamo l'esempio delle birre di prima:

Esempio 3

Supponiamo di avere due agenti, A e B, ed un'ontologia come nell' Esempio 1. Però, questa volta l'agente B chiederà all' agente A:

do_you_like(wine)

Questa volta, l'agente A non riesce a derivare dalla sua base di conoscenza OWL l'informazione "pilsner subclass of wine", poichè l'informazione non c'è. Tuttavia ciò non implica che l'agente A debba rispondere "NO". Infatti, poichè in OWL vale l'assunzione di mondo aperto, è più opportuno che l'agente A risponda qualcosa del tipo:

i_dont_know(wine)

Questo comportamento è il comportamento standard intapreso dal modulo di metaragionamento DALI ( anche se il modulo di metaragionamento vero e proprio non si occupa della relazione di sottoclasse ), in quanto se un agente non riesce ad interpretare il messaggio di un altro, semplicemente non risponde. E' bene che il programmatore DALI tenga a mente queste cose, nel caso in cui voglia definire nuovi predicati che eseguano nuove query sulla base di conoscenza OWL.

3 Ontologie OWL e Metaragionamento DALI

3.1 Richiami sul Metaragionamento DALI

In un sistema multiagente, sebbene molte entità software collaborino al fine di raggiungere un determinato obiettivo, non è detto che tutte parlino lo stesso linguaggio. Un agente logico, però, può ragionare sul messaggio che gli viene in ingresso ed interpretarlo, ovviamente sfruttando le ontologie a propria disposizione.

3.2 Utilizzare OWL nel Metaragionamento DALI

Il vecchio modulo di Metaragionamento DALI supportava un' ontologia semplice quale poteva essere una lista di sinonimi. Ora deve interfacciarsi con OWL, ossia deve interrogare un'ontologia ( mediante SPARQL ) ottenendo così dei risultati ( ed eventualmente ragionarci sopra ).

Vediamo come è fatto il predicato meta e cosa permette di fare:

meta(P,V,AgM):-
	functor(P,F,N),(N=1;N=2),clause(agent(Ag),_),
	clause(ontology(Pre,[Rep,Host],Ag),_),
	if(
		(eq_property(F,H,Pre,[Rep,Host]);
		 same_as(F,H,Pre,[Rep,Host]);
		 eq_class(F,H,Pre,[Rep,Host])
		),true,
		if(clause(ontology(PreM, [RepM,HostM], AgM),_),
			(if(
				(eq_property(F,H,PreM,[RepM,HostM]); 
				 same_as(F,H,PreM,[RepM,HostM]);
				 eq_class(F,H,PreM,[RepM,HostM])
				),true,false)
			),false
		)
	),
	P=..L,substitute(F,L,H,Lf),V=..Lf.

Analizziamo le parti salienti di questo codice:


meta( TermineIniziale, TermineFinale, AgenteMittente )

meta è il predicato che innesca il Metaragionamento DALI. È liberamente modificabile dall'utente ed il suo scopo è TRASFORMARE il messaggio in ingresso in un messaggio che l'agente ricevente possa interpretare secondo la sua base di conoscenza.


eq_property(FuntoreIniziale, FuntoreFinale, Prefissi, [Repository, Host])

È un predicato definito nel modulo di comunicazione con le ontologie. Questo predicato fallisce se il funtore ( del termine appartenente al messaggio che non riusciamo a decifrare ) FuntoreIniziale non è collegato, mediante la relazione "owl:equivalentProperty" ad una proprietà OWL FuntoreFinale. I Prefissi sono le stringe "PREFIX" che bisogna anteporre alla query SPARQL affinchè tutte le abbreviazioni di namespace siano risolte. Repository è il repository SESAME ( che può integrare, o meno, un motore di inferenza ) dove è presente l'ontologia da utilizzare. Host invece rappresenta l'host da interrogare ( ossia la macchina sulla quale gira SESAME ).

Diamo un breve esempio della query SPARQL sottostante il processo descritto sopra:

Esempio 4

Supponiamo di avere due agenti, A e B, e supponiamo anche che l'agente A contenga all'interno della sua base di conoscenza il seguente predicato:

age(pippo,50).

Supponiamo che lo stesso concetto di età sia noto all'agente B, ma che indichi questo concetto con il predicato "hasage". Allora, alla domanda dell'agente B:

hasage(pippo,50) ?

senza un'ontologia, A non sarebbe in grado di rispondere alla domanda di B.

Supponiamo di avere un'ontologia in cui ci si riferisce ad una stessa proprietà con due sintassi differenti:

:HasAge owl:equivalentProperty :Age

Nell'ontologia sopra c'è l'informazione che Age e HasAge indicano lo stesso concetto. Dobbiamo però tirare fuori questa informazione dall'ontologia e presentarla all'agente in un formato che egli possa comprendere. Qui ci viene in aiuto il mapping tra atomi prolog e URI definito nel Capitolo 2.
Poichè un atomo prolog può corrispondere alla parte Nome di un URI, cerchiamo tutti le risorse equivalenti agli URI la cui parte Nome è "hasage":

SELECT ?O WHERE { ?S owl:equivalentProperty ?O. FILTER ( ( isIri(?S) && REGEX(str(?S), "[/ #]hasage$","i") ) ). }

Eliminando la parte Locazione dall'URI che comporrà la risposta alla query sopra ( e normalizzando la parte Nome ) otteniamo "age". Quindi, sostituendo il predicato "hasage" di B con il predicato "age", l'agente B sarà in grado di interpretare correttamente il messaggio. Chi si occupa di effettuare questa sostituzione, è ( come si vede dal codice ) la funzione meta.



same_as(FuntoreIniziale,FuntoreFinale,Prefissi,[Repository,Host]);


Del tutto simile al predicato "eq_property", solo che questa volta la query SPARQL che verrà effettuata restituirà un INDIVIDUO FuntoreFinale che è equivalente all'INDIVIDUO FuntoreIniziale. La chiamata a "same_as" fallisce se questa query non ha successo. Forniamo di seguito un breve esempio riguardante la query SPARQL che viene effettuata.

Esempio 5

Supponiamo di avere due agenti in una situazione del tutto analoga a quella descritta precedentemente nell' Esempio 4.

Supponiamo inoltre di avere un'ontologia in cui ci si riferisce ad uno stesso individuo in due maniere differenti.


:Mary owl:sameAs :MaryBrown

A questo punto la query che verrà effettuata dalla libreria di comunicazione, sarà:


SELECT ?O WHERE { ?S owl:sameAs ?O. FILTER ( ( isIri(?S) && REGEX(str(?S), "[/ #]mary$","i") ) ). }

Se l'agente A ha nella sua base di conoscenza un predicato "marybrown", allora A può comprendere il contenuto del messaggio mandato da B e comportarsi di conseguenza.


eq_class(FuntoreIniziale,FuntoreFinale,Prefissi,[Repository,Host]);


Simile ai primi due, con la differenza che la query SPARQL si preoccuperà di trovare una CLASSE FuntoreFinale che sia equivalente ( sempre nel senso OWL del termine ) alla CLASSE FuntoreIniziale.

Esempio 6

Supponiamo di avere due agenti, A e B, esattamente come negli esempi precedenti.

Supponiamo di avere un'ontologia in cui ci si riferisce ad una stessa CLASSE in due maniere differenti:

:Auto owl:equivalentClass :Automobile

La libreria di comunicazione cercherà un sinonimo di "Auto" nell'ontologia, mediante la seguente query SPARQL:

SELECT ?O WHERE { ?S owl:equivalentClass ?O. FILTER ( ( isIri(?S) && REGEX(str(?S), "[/ #]auto$","i") ) ). }

La funzione meta quindi trasformerà funtore "auto" nel funtore "automobile" cosicchè A possa interpretare il messaggio mandato da B.


Anche se ormai dovrebbe essere chiaro come funziona il modulo di metaragionamento, per completezza analizziamo la funzione meta che si occupa dei predicati simmetrici:


meta(P,V,_):-functor(P,F,N),N=2,symmetric(F),P=..L,
		delete(L,F,R),reverse(R,R1),
		append([F],R1,R2),V=..R2.

L'unica funzione nuova in questo predicato meta è il predicato

symmetric(FuntoreIniziale)

che verifica se la proprietè FuntoreIniziale è simmetrica nell'ontologia OWL associata all'agente.

Qui sotto forniamo un'esempio della query SPARQL associata al predicato symmetric ( che come tutti i predicati che si interfacciano con le ontologie OWL, è nel modulo di comunicazione con le ontologie ):

ASK { ?S rdf:type owl:SymmetricProperty. FILTER ( ( isIri(?S) && REGEX(str(?S), "[/ #]FuntoreIniziale$","i") ) ). }


Potrebbe succedere che il predicato sia simmetrico, ma che comunque non sia presente nella base di conoscenza dell'agente ricevente. In questo caso, bisogna andare a cercare possibili sinonimi in una delle due ontologie ( Ricevente e Mittente ). Ovviamente, un predicato meta è definito anche per gestire questa eventualità:


meta(P,V,AgM):-
	clause(agent(Ag),_),functor(P,F,N),N=2,(symmetric(F,AgM);symmetric(F)), P=..L,
	delete(L,F,R),reverse(R,R1),
	clause(ontology(Pre,[Rep,Host],Ag),_),
	if(
		(eq_property(F,Y,Pre,[Rep,Host]);
		 same_as(F,Y,Pre,[Rep,Host]);
		 eq_class(F,Y,Pre,[Rep,Host])
		),true,
		if(clause(ontology(PreM, [RepM,HostM], AgM),_),
			(if(
				(eq_property(F,Y,PreM,[RepM,HostM]); 
				 same_as(F,Y,PreM,[RepM,HostM]);
				 eq_class(F,Y,PreM,[RepM,HostM])
				),true,false)
			),false
		)
	),          
	append([Y],R1,R2),V=..R2.


4 La libreria di comunicazione con le ontologie

4.1 Introduzione alla libreria

La libreria di comunicazione con le ontologie, così come accennato nel capitolo 1, si pone da tramite tra l'agente DALI e le ontologie OWL. Questa libreria si compone di due livelli: Per poter capire come si può adattare il Livello Utente alle necessità dei nostri agenti DALI, analizziamo nel dettaglio le funzionalità messe a disposizione dal livello sottostante.

4.2 Livello Utilità

Questo livello mette a disposizione dei predicati utili al sovrastante Livello Utente.

Riprendiamo l' esempio fatto precedentemente a proposito del metaragionamento (Esempio 4):

SELECT ?O WHERE { ?S owl:equivalentProperty ?O. FILTER ( ( isIri(?S) && REGEX(str(?S), "[/ #]hasage$","i") ) ). }

Dobbiamo domandarci almeno due cose: Mediante lo SPARQL TEMPLATE FILE si risponde ad entrambe le domande. Esso non è altro che un file contentente templates di possibili query, ognuna contenente dei placeholders ( indicati dal simbolo % ) che verranno sostituiti, al momento in cui la query dovrà essere eseguita, da valori appropriati.

Per chiarire meglio i concetti, questo è lo SPARQL TEMPLATE FILE di default:

"SUBCLASS".
"ASK { ?S rdfs:subClassOf ?P. 
  FILTER ( 
    ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") )  
  ). 
  FILTER ( 
    ( isIri(?P) && REGEX(str(?P), \"[/ #]%$\",\"i\") ) || ( isLiteral(?P) && REGEX(str(?P), \"^%$\",\"i\") ) 
   ). 
}".
"SUBPROPERTY".
"ASK { ?S rdfs:subPropertyOf ?P. 
   FILTER (
     ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") )  
   ). 
   FILTER ( 
     ( isIri(?P) && REGEX(str(?P), \"[/ #]%$\",\"i\") ) || ( isLiteral(?P) && REGEX(str(?P), \"^%$\",\"i\") ) 
   ). 
}".
"EQ_PROPERTY".
"SELECT ?O WHERE { ?S owl:equivalentProperty ?O. 
   FILTER ( 
     ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") )  
   ). 
}".
"SAME_AS".
"SELECT ?O WHERE { ?S owl:sameAs ?O. 
   FILTER ( 
     ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") )  
   ). 
}".
"EQ_CLASS".
"SELECT ?O WHERE { ?S owl:equivalentClass ?O. 
   FILTER ( 
     ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") )  
   ). 
}".
"SYMMETRIC".
"ASK { ?S rdf:type owl:SymmetricProperty. 
   FILTER ( 
     ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") )  
   ). 
}".

Come si vede in figura, ogni query è preceduta da una label che la identifica in maniera UNIVOCA.

È messo in evidenza, inoltre, il simbolo di placeholder %. I placeholder verranno sostituiti con le stringhe opportune, così come mostreremo più avanti.

A differenza di una normale query SPARQL, le query in questo file contengono il carattere di escape "\" affinché l'interprete ignori il carattere doppi apici.

Possiamo notare come ogni query faccia un uso più o meno standard dell'espressione FILTER. Questo perchè ogni query deve essere fatta tenendo a mente le considerazioni fatte al Capitolo 2.

Per chiarire meglio, esaminiamo una espressione FILTER tra quelle sopra:

FILTER ( 
   ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") ) || ( isLiteral(?S) && REGEX(str(?S), \"^%$\",\"i\") ) 
).

Innanzitutto si osserva che l'espressione è del tipo A || B. Il termine A si occupa degli URI, mentre il termine B si occupa dei Letterali.

Il termine A sostanzialmente dice : "Trova tutti gli URI in cui la parte Nome coincide ( CASE_INSENSITIVE ) con la stringa %" .

Sostituendo il simbolo % con un atomo prolog denormalizzato, l'effetto pratico è che FILTER fa corrispondere a questo atomo un URI, effettuando quindi quel mapping di cui si parlava nel paragrafo 2.2.

Se ad esempio vogliamo cercare tutti gli pseudonimi dell'atomo "maria", dobbiamo prendere tutti gli URI la cui parte Nome è "maria" ( CASE INSENSITIVE ) e che sono legati mediante una relazione di owl:sameAs alle risorse cercate.

Possiamo estendere tutte le considerazioni fatte per il termine A al termine B. L'unica differenza, è che il termine B si occupa delle risorse che sono dei Letterali.

Diamo un'esempio più completo:

Esempio 7

Nell'Esempio 1 abbiamo spiegato a grandi linee come può funzionare un'interazione tra gli agenti DALI ed un'ontologia OWL. Adesso che abbiamo introdotto le convenzioni riguardanti gli URI ed il file di template SPARQL, possiamo approfondire la cosa:

Supponiamo di avere un'ontologia OWL in cui è presente la tripla

http://my_ontology/#Pilsner/ rdfs:subClassOf http://my_ontology/#Beer/.

e di avere un agente A con il seguente codice:

do_you_likeE(Y,Mit):-once(reply(Y,Mit)).

reply(Y,Mit):-
	clause(agent(Agent),_),
	once(like(Y)), 
	messageA(Mit,send_message(i_like, Agent)).
	
reply(Y,Mit):-
	clause(agent(Agent),_),
	messageA(Mit,send_message(i_dont_know(Y), Agent)).

like(pilsner).
like(X):-if(var(X),(!,false),true), like(Y), subclass(Y,X,beer_1). 

Allora alla domanda di un altro agente B

do_you_like(beer).

l'agente A cercherà di rispondere all'evento esterno do_you_like scatenato da B.

La reazione all'evento esterno do_you_like coinvolge la verifica del valore di verità like(beer).

Ora, vediamo che like(beer) non è direttamente disponibile nel codice di A ( al contrario di like(pilsner) ), però si può dedurre il predicato like(beer) verificando la vericidità del predicato subclass .

Il predicato subclass, è un predicato scritto dall'utente nella parte modificabile della libreria di comunicazione con le ontologie ( quindi nel cosiddetto Livello Utente ).

Per mezzo della chiamata a subclass, l'agente A effettuerà i seguenti passi: Abbiamo ottenuto una query di tipo ASK che chiede se pilsner è sottoclasse di beer, considerando unicamente la parte Nome dgli URI. Ricordiamo che la query ASK può rispondere solo "true" oppure "false".
Ma quali predicati utilizza il predicato subclass per eseguire i passi 1 e 2 ?

Il predicato, appartenente al Livello Utilità, che si occupa, data una label "Mode" ed una lista di stringhe [Marker1, ..., MarkerN], di recuperare una certa query dallo SPARQL TEMPLATE FILE, è il predicato query:

query(Query, [Marker1, ..., MarkerN], Mode)

Il primo argomento rappresenta la query in cui sono stati sostituiti tutti i placeholders ( così come abbiamo appena visto con l'Esempio 7 ), ossia ciò che la chiamata al predicato query restituisce.

Il secondo argomento, è una lista di stringhe che dovranno essere sostituite al posto dei placeholders incontrati nella query. L'ordine è importante, in quanto il primo elemento "Marker1" della lista andrà a sostituire il PRIMO placeholder "%" nella query . Il secondo elemento andrà a sostituire secondo placeholder, e così via.

Il terzo argomento, Mode, corrisponde ad una lista di codici che rappresenta l'etichetta della query desiderata.

Dovrebbe apparire ora chiaro che, per ottenere una query come quella fatta all' Esempio 7, dobbiamo effettuare la seguente chiamata al predicato query:

query(Result, [pilsner, beer, beer], "SUBCLASS")

Dopo la chiamata il valore contenuto in Result sarà :

"SUBCLASS".
"ASK { ?S rdfs:subClassOf ?P. 
	FILTER ( ( isIri(?S) && REGEX(str(?S), \"[/ #]pilsner$\",\"i\") ) ). 
	FILTER ( ( isIri(?P) && REGEX(str(?P), \"[/ #]beer$\",\"i\") ) 
	|| 
	( isLiteral(?P) && REGEX(str(?P), \"^beer$\",\"i\") ) ). 
}".

Nota: Si ricorda che in SICSTUS la sintassi tra virgolette "String" è equivalente ad una lista di codici ASCII il cui contenuto è composto dalle lettere che compongono la stringa String.

Ora abbiamo la giusta query da sottoporre a SESAME. Anzi, no. Infatti mancano ancora le dichiarazioni dei prefissi da anteporre alla query da effettuare. I prefissi sono asseriti nella memoria dell'agente, per mezzo del predicato ontology:

ontology(Prefixes, [Repository,Host], Agent)

Il predicato ontology ha 3 argomenti:

Ad ogni agente può essere associato un determinato predicato ontology, i cui campi saranno letti dal file indicato nelle direttive di inizializzazione per quell'agente ( il file solitamente è contenuto nella cartella onto ).

Ciò non significa, tuttavia, che l'agente mantenga asserito un solo predicato ontology per volta. Infatti l'agente durante la sua vita può "assorbire" le ontologie degli agenti con cui si relaziona. Questi saranno nuovi predicati ontology che andranno ad arricchire la base ontologica dell'agente. Ricordiamo, però, che in base alle considerazioni fatte nel Capitolo 2, questa nuova conoscenza potrà essere utilizzata solo nel caso in cui si debba comunicare con l'agente da cui è stata assorbita l'ontologia. Questa limitazione è eliminabile se si considereranno gli URI interamente, invece che limitarsi alla sola parte nome. Questi elementi potranno essere presi in considerazione per eventuali ampliamenti futuri di questo lavoro.

Benissimo, adesso abbiamo una query e sappiamo dove andare a prendere le informazioni riguardanti i prefissi ed il server SESAME da interrogare.

Il predicato create_query_url si occupa di creare l'url adatto per poter fare la richiesta GET HTTP al server SESAME:


create_query_url(Query, Prefixes, Repository, QueryCodes)


Il predicato ha 4 parametri:

Il predicato che permette di eseguire una query, data una lista di caratteri ascii Query ed una stringa del tipo 'hostname:port' Host, è il predicato do_query:


do_query(QueryCodes, Host, Response)


Response conterrà la risposta del server SESAME. La risposta, così come abbiamo visto nella figura riguardante l'architettura dell'applicazione, sarà in un particolare formato XML.

Nel caso la query sia stata una ASK, allora il Response XML sostanzialmente conterrà una risposta di tipo "true" oppure "false". Per ricavare la risposta ad una query di tipo ASK data una risposta in XML Response si utilizza il predicato fetch_ask:


fetch_ask(Response, Bool)


Bool è una lista di caratteri ASCII che può assumere i valori di "true" oppure "false".

Ma non tutte le query sono di tipo ASK. Le query di tipo SELECT ritornano un documento XML rappresentante i bindings che ci sono tra le variabili appartenenti alla parte SELECT CLAUSE della SELECT ed i valori trovati.

Abbiamo bisogno quindi di tradurre questi "bindings" contenuti nella risposta XML in predicati prolog. Il predicato che si occupa di ciò, data in input una risposta "Response" SESAME a seguito di una richiesta di tipo SELECT, è il predicato

fetch_select(Response,ResultsList).

L'output "ResultList" della fetch_select è una lista di elementi Risultato. Ogni Risultato è un INSIEME DI BINDINGS SULLE VARIABILI specificate dalla parte SELECT CLAUSE della SELECT.

Un elemento Risultato è rappresentato dal seguente predicato:

result(BindList)

BindList è la lista di bindings propria di quel risultato. Il formato di una lista di bindings è una lista di predicati binding

BindList=[binding(Var, Type, Value), ..., binding(Var, Type, Value)]

Ogni predicato binding rappresenta un binding tra una variabile Var di Tipo Type ed il suo valore Value. Type può essere "uri", "bnode" oppure "literal".

Se il Tipo della variabile restituita da una SELECT è "uri", vorrà dire che biognerà ricavare la parte nome di quell'URI, portare tutto al LOWER_CASE e sostituire tutti gli spazi bianchi all'interno della parte nome dell'URI con degli UNDERSCORE, così come già spiegato nel Capitolo 2.

Inoltre ho bisogno di una funzione che permetta di accedere facilmente ai valori delle singole variabili per ogni risultato che si sta' analizzando. Questo compito, ed il precedente, sono svolti dal predicato normalize:

normalize(Var, Result, Value)

La funzione normalize dato un elemento risultato Result ed una lista Var di codici rappresentanti una variabile contenuta nella SELECT CLAUSE di una query di tipo SELECT, restituisce il Valore Value NORMALIZZATO ( vedi 2.2 ) associato a quella variabile nella query.

Esempio 8

Forniamo qui di seguito l'implementazione, all'interno del Livello Utente della libreria di comunicazione, del predicato "subclass" relativo all' Esempio 7:

subclass(Y,X,AgM):-
	clause(agent(A),_), 
	clause(ontology(Prefixes,[Repository,Host],A),_);clause(ontology(Prefixes,[Repository,Host],AgM),_),
	query(Query,[X,Y,Y],"SUBCLASS"),
	create_query_url(Query,Prefixes,Repository,QueryCodes),
	do_query(QueryCodes,Host,Response),
	fetch_ask(Response,Bool),
	if(Bool="true",true,false).

È ora facile commentare questo codice. Come prima cosa si vede che l'agente prende in considerazione unicamente la propria ontologia e quella dell'agente mittente del messaggio. Poi l'agente, mediante il predicato query, cerca la query con etichetta "SUBCLASS" all'interno dello SPARQL TEMPLATE FILE e sostituisce i placeholders all'interno della query con i valori delle variabili Y e X.

Il terzo passo si concretizza nella creazione dell'URL da associare alla richiesta GET, utilizzando i prefissi e gli host indicati dai predicati ontology di mittente e ricevente. Quindi l'agente effettua la query vera e propria e mette la risposta HTTP nella variabile Response

La variabile Response verrà esaminata dal predicato fetch_ask, il quale eseguirà il parsing della risposta in XML ed estrapolerà il valore ( "true" o "false" ) della risposta, mettendolo nella variabile Bool.

Se la risposta è "true", allora la valutazione del predicato subclass andrà a buon fine. Altrimenti, il predicato fallirà.

Lo SPARQL non consente la composizione di query "SELECT", o la consente parzialmente mediante il costrutto "CONSTRUCT". Componendo due query si possono utilizzare i risultati di una per create il "triple pattern" dell'altra. Questa possibilità è messa a disposizione dal livello utilità mediante il predicato compose_triple_pattern_uri:

compose_triple_pattern_uri(Subject, Predicate, Object, Delimiter, TriplePatternResult)

Subject, Predicate e Object sono tre URI che rappresentano rispettivamente il Soggetto, il Predicato e l'Oggetto del triple pattern che si intende comporre. Se Subject o Predicate ( o entrambi ) sono "[]", allora il triple pattern sarà nella forma abbreviata. Delimiter può essere uno tra ";", "." e "," e rappresenta il simbolo di punteggiatura che delimita la tripla. In TriplePatternResult andrà la tripla così creata.

In abbinamento al predicato sopra è utile un predicato che permetta di prendere il risultato di una query senza normalizzarlo. Il predicato in questione è take_result:

take_result(Var, Result, Value)

I parametri sono gli stessi del predicato normalize. La differenza sta nel fatto che in Value andrà messo il valore della variabile Var ( contenuta nel risultato della query Result ) NON NORMALIZZATO.

Esempio 9

Supponiamo di avere un'ontologia dei cibi così formata:

:SpaghettiChitarra :hasDrink :_blank1.
:_blank1           :hasColor :Red;
                   :hasBody  :Full.
:PastaVongole      :hasDrink :_blank2.
:_blank2           :hasColor :White.

Supponiamo anche di avere un'ontologia dei vini a disposizione, nella quale è presente un elenco di vini e le loro caratteristiche:

:Chianti   :hasColor :White.
:Barbera   :hasColor :Red;
           :hasBody  :Full.
:Cerasuolo :hasColor :Red;

Supponiamo di voler ricavare un vino adatto agli Spaghetti alla Chitarra. L'ontologia dei cibi può essere sfruttata per ricavare le proprietà che deve possedere un vino adatto al piatto desiderato. Intendiamo utilizzare queste proprietà per cercare nell'ontologia dei vini un vino adatto. Per poter fare una cosa del genere, abbiamo ovviamente bisogno di due query SELECT: la prima che ci tira fuori le proprietà, la seconda che a partire dalle proprietà individuate dalla prima ci tira fuori i vini.

SELECT ?Property ?Value WHERE 
	{ ?S :hasDrink []; 
	     ?Property ?Value.
		FILTER ( ( isIri(?S) && REGEX(str(?S), "[/ #]SpaghettiChitarra$","i") ) ). 
	}

Questa query ritornerà il seguente risultato:

PropertyValue
:hasColor:Red
:hasBody:Full

Quello che possiamo fare ora è sfruttare i risultati di questa SELECT per eseguire una nuova SELECT, in cui definiamo il triple pattern del costrutto WHERE mediante il predicato compose_triple_pattern_uri appena introdotto. Supponiamo di aver stabilito il seguente pattern di query all'interno dello Sparql Template File:

"Proprietà->Vini".
"SELECT DISTINCT $Wine WHERE { $Wine % }".

Andiamo quindi a comporre il triple pattern che andrà messo al posto del placeholder %. Le informazioni per poterlo comporre sono contenute all'interno del risultato della prima SELECT. Supponiamo che questo risultato ( ricordiamo, in formato XML ) sia contenuto all'interno della variabile "Response". Allora il codice per comporre il triple_pattern sarà pressappoco così:

fetch_select(Response,ResultsList),!,   %%In Response c'è la risposta in XML
assert(result_l([])),
last(ResultsList, Last),
repeat,
   member(Result, ResultsList),
   
   %%Prendi i risultati senza normalizzarli.
   take_result("Value",Result, Uri), 
   take_result("Property",Result, Prop),

   %%Se è l'ultimo risultato, allora chiudi il triple pattern con il "."
   if(Result=Last, 
     compose_triple_pattern_uri([], Prop, Uri, ".", GraphPattern1),
     compose_triple_pattern_uri([], Prop, Uri, ";", GraphPattern1)
   ),
   clause(result_l(List),_),                
   append(List, GraphPattern1, List1), 
   retractall(result_l(List)), assert(result_l(List1)),
Result=Last,
!,
 clause(result_l(ListaProp), _),retractall(result_l(_)), 
      
%%Aggiungi gli apici al triple pattern, poichè il placeholder va' sostituito con una 'stringa'.
append("'", ListaProp, ListaProp1),
append(ListaProp1, "'", ListaProp2),
atom_codes(ListaPropNamed,ListaProp2).

Andando quindi a sostituire "ListaPropNamed" all'interno del placeholder della query con etichetta "Proprietà->Vini", otteniamo:

"Proprietà->Vini".
"SELECT DISTINCT $Wine WHERE { $Wine :hasColor :Red;
                                     :hasBody  :Full.
                             }".

Approfondimenti sugli altri predicati standard contenuti nel Livello Utente della libreria di comunicazione, inclusi gli esempi sulle SELECT, verranno introdotti nel prossimo paragrafo.

4.2 Livello Utente

Il Livello Utente si occupa di fornire agli agenti predicati che permettono di sfruttare le ontologie OWL. Poichè le applicazioni sono pressoccè infinite, questo modulo è liberamente espandibile secondo i bisogni dell'utente.

Un utente può creare nuovi predicati per la gestione delle ontologie, e nuove query, rispettivamente sfruttando le funzionalità messe a disposizione dal sottostante Livello Utilità ed aggiungendo nuove label e nuove query allo SPARQL TEMPLATE FILE.

Al fine di dare alcuni esempi di come ciò può essere fatto, descriviamo qui di seguito dei predicati che di default fanno parte di questo livello.


subproperty(Y,X):-
	if(var(X),false,true),
	clause(agent(A),_), 
	clause(ontology(Prefixes,[Repository,Host],A),_);clause(ontology(Prefixes,[Repository,Host],AgM),_),
	query(Query,[X,Y],"SUBPROPERTY"),
	create_query_url(Query,Prefixes,Repository,QueryCodes),
	do_query(QueryCodes,Host,Response),
	fetch_ask(Response,Bool),  
	if(Bool="true",true,false).


Il predicato subproperty è del tutto analogo al predicato subclass, eccetto per il fatto che questo predicato fallisce se Y non è una sottoproprietà di X.

La query con etichetta "SUBPROPERTY" all'interno dello SPARQL TEMPLATE FILE è:


"ASK { ?S rdfs:subPropertyOf ?P. 
	FILTER ( ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") ) ). 
	FILTER ( ( isIri(?P) && REGEX(str(?P), \"[/ #]%$\",\"i\") ) 
	|| 
	( isLiteral(?P) && REGEX(str(?P), \"^%$\",\"i\") ) ). 
}".


Così come il predicato subclass, questo predicato non viene utilizzato nel modulo di metaragionamento, ma deve essere invocato esplicitamente dall'utente all'interno del programma DALI. Più avanti si addurranno le motivazioni che hanno portato a questa scelta.


eq_property(Y,X,Prefixes,[Repository,Host]):-
        query(Query,[Y],"EQ_PROPERTY"),
		  create_query_url(Query,Prefixes,Repository,QueryCodes),
   	  do_query(QueryCodes,Host,Response),         
		  fetch_select(Response,ResultsList),!,
        member(Result, ResultsList),
        normalize("O",Result, X).


Il predicato eq_property è un predicato che viene invocato dal modulo di metaragionamento, e che abbiamo già incontrato nella sezione 3.2.

A differenza dei predicati che abbiamo trattato finora, che erano delle ASK, questo è il primo esempio che incontriamo dell'utilizzo della funzione fetch_select.

Abbiamo già detto che la fetch_select prende la risposta XML del server SESAME e la converte in una lista di predicati result. In questo esempio si può vedere come il predicato eq_class prenda un elemento da questa lista e lo normalizzi utilizzando la già introdotta funzione normalize.

La query cone etichetta "EQ_PROPERTY" all'interno dello SPARQL TEMPLATE FILE è:


"SELECT ?O WHERE { ?S owl:equivalentProperty ?O. FILTER ( ( isIri(?S) && REGEX(str(?S), \"[/ #]%$\",\"i\") ) ). }".


Come si può notare, la parte SELECT CLAUSE della query contiene la sola variabile O. Ciò spiega perchè il primo argomento della funzione normalize sia "O", in quanto noi stiamo cercando i binding per quella variabile.

Nel caso la SELECT CLAUSE contenga più variabili, per ognuna di esse va' invocata una normalize.

Analoghi sono i restanti predicati di default, e comunque l'intera libreria è consultabile nell'appendice di questo documento.

5 Creare un agente con le ontologie

La finalità di questo capitolo è di mostrare quali siano i passi da seguire per poter creare un agente dotato di ontologie OWL.

5.1 Gli agenti della birra

Questo esempio mira a far comprendere come si possa utilizzare il predicato subclass ( appartenente alla libreria di comunicazione con le ontologie ) all'interno di un agente DALI.

Questo esempio ricalca l'esempio già affrontato nell' Esempio 1.

Il primo agente, beer_1, ha il seguente programma DALI:


goE:>once(ask).

ask:-clause(agent(Z),_), friend(Dest),
				  messageA(Dest, send_message(do_you_like(beer,Z),Z)).

i_likeE:-okA.

i_dislikeE:-very_badA.

friend(beer_2).


Questo agente sostanzialmente chiede all'agente beer_2 se gli piace la birra.

Vediamo nel dettaglio il codice dell'agente beer_2:


do_you_likeE(Y,Mit):>once(reply(Y,Mit)).

reply(Y,Mit):-
	clause(agent(Agent),_),
	once(like(Y)), 
	messageA(Mit,send_message(i_like, Agent)).
	
reply(Y,Mit):-
	clause(agent(Agent),_),
	messageA(Mit,send_message(i_dont_know(Y), Agent)).

like(pilsner).
like(X):-if(var(X),(!,false),true), like(Y), subclass(Y,X,beer_1). 


Per tutte le cose dette nei capitoli precedenti, appare ben chiaro cosa fanno questi due agenti.
Vediamo come è fatto il file di inizializzazione dell'agente beer_2:


agent('../program/beer_2',beer_2,'no',english,['../communication'],
		['../communication_fipa','../learning',
		'../planasp'],'no','../onto/dali_onto.txt',[]
).


I campi di questo predicato dovrebbero già essere noti al lettore. A noi interessa il terzo campo. Nel terzo campo, infatti, va indicato il path relativo del file contenente le informazioni che dovranno essere asserite nel predicato ontology di quell'agente.

In questo esempio, 'no' indica che l'agente non ha una propria ontologia. Ciò naturalmente non è un problema, in quanto le informazioni che servono a beer_2 per rispondere alla domanda di beer_1 gliele fornirà l'ontologia di beer_1.

Infatti, il file di inizializzazione per l'agente beer_1 è il seguente:


agent('../program/beer_1',beer_1,'../onto/beer_1.onto',english,['../communication'],
		['../communication_fipa','../learning',
		'../planasp'],'no','../onto/dali_onto.txt',[]
).


A differenza del file di inizializzazione di beer_2, in questo caso nel terzo campo è indicato il path contenente le informazioni necessarie per interrogare l'ontologia cui fa riferimento l'agente beer_1.

Il file, "beer_1.onto" presenta il seguente contenuto:

"PREFIX dc:\nPREFIX rdfs:\nPREFIX jms:\nPREFIX owl:\nPREFIX xsd:\nPREFIX rdf:\nPREFIX rss:\nPREFIX daml:\nPREFIX vcard:\n\n".
"/openrdf-sesame/repositories/beer2".
"localhost:8080".

La prima riga di questo file è formata dai prefissi, la seconda rappresenta il repository dove è contenuta l'ontologia, e la terza è la stringa che rappresenta l'host su cui gira SESAME

Un repository può facilmente essere creato sfruttando l'interfaccia HTML di SESAME, e la prima riga di questo file può essere creata copiando ed incollando i prefissi che SESAME mette di default ad ogni query che si voglia fare, mediante l'interfaccia HTML, sul repository appena creato.

Lo SPARQL TEMPLATE FILE è contenuto nella cartella "owl", e si chiama query.conf.

L'ontologia utilizzata per questo esempio è disponibile a questo indirizzo

5.2 Gli agenti dell' età

Questo esempio si propone di mostrare come funziona il modulo di metaragionamento integrato in DALI.

In questo esempio ci sono due agenti, family_1 e family_2. Il primo agente fa al secondo agente una domanda. Il secondo agente non può capire la domanda del primo agente se non sfruttando il metaragionamento.

Scendiamo nel dettaglio, e mostriamo il codice dell'agente family_1:


goE:>once(check).

check:-clause(agent(Z),_), friend(Dest),
				  messageA(Dest, send_message(hasAge(anne,23, Z),Z)).

friend(family_2_2).


Il codice per l'agente family_2 è invece il seguente:


ageE(Person,Age, Mitt):>once(check_age(Person,Age, Mitt)).

check_age(P,A, Mitt):-clause(agent(Z),_), is_old(P,A), messageA(Mitt, send_message(right, Z). 

check_age(P,A, Mitt):-clause(agent(Z),_), messageA(Mitt, send_message(wrong, Z). 

is_old(anne, 23).

Si può vedere che l'evento esterno di questo agente non è hasage, bensì age. Bisogna quindi sfruttare il metaragionamento, e capire se è hasage può essere sostituito con age, al fine di poter reagire all'evento.

Tutta questa fase è fatta in maniera trasparente dal modulo del metaragionamento, e se interrogando l'ontologia OWL il metaragionento scoprirà che hasage e age sono concetti equivalenti, allora l'agente intraprenderà la reazione.

In questo esempio è l'agente family_2 ad essere dotato di un'ontologia, ed il contenuto del file contenente le informazioni per interrogare l'ontologia è:


"PREFIX rdfs:\nPREFIX xsd:\nPREFIX owl:\nPREFIX rdf:\nPREFIX otherOnt:".
"/openrdf-sesame/repositories/family".
"localhost:80".


Mentre l'ontologia utilizzata nell'esempio è visionabile qui.

5.3 L'agente symmetric

Questo esempio mostra un agente che utilizza il metaragionamento per interpretare un predicato binario che non capisce, ma che può interpretare se l'ordine dei suoi argomenti viene invertito. Ricordiamo che, prima di poter invertire gli argomenti di un predicato binario, bisogna verificare che il predicato sia simmetrico.

Mostriamo il codice DALI dell'agente symmetric:


hasSpouseE(pino,pina):>auguriA(pino,pina).


All'interno dell' ontologia di questo esempio è presente la seguente tripla:


http://example.com/owl/families/hasSpouse rdf:type owl:SymmetricProperty


La tripla sopra indica che hasSpouse è una proprietà simmetrica. Quindi, se mandiamo all'agente il seguente messaggio:


hasSpouse(pina, pino)


L'agente capirà il messaggio ( grazie al predicato symmetric invocato all'interno del modulo di metaragionamento ) e reagirà all'evento.

6 Considerazioni Finali

6.1 Potenzialità dell'approccio adottato

L'approccio adottato separa la parte riguardante le ontologie dalla parte riguardante le basi di conoscenza degli agenti. Un'ontologia potrebbe essere su un'altra macchina rispetto alla macchina dove risiede l'agente che ne fa utilizzo, non andando quindi ad inficiare le caratteristiche proprie di un sistema, che per sua natura è distribuito, come quello multiagente.

Altra caratteristica interessante è il fatto che non si mescolino assieme predicati PROLOG e query SPARQL. Le query SPARQL hanno un loro file dove poter essere definite, il prolog si scrive altrove.

Un altro punto di forza di questo approccio al problema riguarda lo SPARQL. Installando un reasoner sul repository semantico dove l'ontologia è contenuta, infatti, possiamo estrarre conoscenza implicita dalle ontologie senza dover gestire questo "ragionamento" esplicitamente tramite il prolog. Il ragionamento verrà effettuato esternamente all'agente, in maniera del tutto trasparente.

Facciamo un breve esempio riguardo le potenzialità di un reasoner:

Esempio 9

Supponiamo di avere un'ontologia, e tanto per essere monotoni riprendiamo l'ontologia della birra di cui abbiamo abusato in queste pagine:


:BottomFermentedBeer rdf:type owl:Class
:BottomFermentedBeer rdfs:subClassOf :Beer
:Pilsner rdf:type owl:Class
:Pilsner rdfs:subClassOf :Beer
:Beer rdf:type owl:Class


Ora, se io chiedo in SPARQL quali sono le sottoclassi di Beer, la risposta sarà:


:BottomFermentedBeer


Perchè "Pilsner" non è risultata essere una sottoclasse di "Beer" ? Semplicemente perchè questa informazione, sebbene inferibile, non è DIRETTAMENTE disponibile come tripla nell'ontologia, ossia l'unica sottoclasse diretta di "Beer" è "BottomFermentedBeer".

Per far sì che anche "Pilsner" venga considerata per quello che è, ossia per una sottoclasse ( sebbene indiretta ) di "Beer", abbiamo bisogno di un reasoner che supporti la TRANSITIVITÀ. In SESAME è sufficiente caricare l'ontologia su di un repository di tipo "In Memory Store RDF Schema and Direct Type Hierarchy" per ottenere l'effetto desiderato.