misc/aqe: Initial import.
This commit is contained in:
		
							parent
							
								
									c5401e5128
								
							
						
					
					
						commit
						b51e9456c6
					
				| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
## AQE: A Query Engine
 | 
			
		||||
 | 
			
		||||
This is an implementation of a knowledge base, hacked together in Python
 | 
			
		||||
3 (it won't work in Python 2 for reasons of modules) for now to quickly
 | 
			
		||||
iterate on ideas.
 | 
			
		||||
 | 
			
		||||
There are a few key points:
 | 
			
		||||
 | 
			
		||||
+ A `KnowledgeBase` contains facts.
 | 
			
		||||
+ A fact is a tuple: (relationship, subject, object). For example,
 | 
			
		||||
  `('is', 'sky', 'blue')`.
 | 
			
		||||
+ A `KnowledgeBase` has three core methods: ask, retract, and tell.
 | 
			
		||||
+ The `ask` method queries the `KnowledgeBase` to ascertain whether
 | 
			
		||||
  a fact is true. Either the subject or the object may be `None`,
 | 
			
		||||
  in which case all satisifiable facts are returned.
 | 
			
		||||
+ The `retract` method tells the `KnowledgeBase` that the fact is
 | 
			
		||||
  no longer true. If it's rainy, we might retract our fact about the
 | 
			
		||||
  sky being blue.
 | 
			
		||||
+ The `tell` method tells the `KnowledgeBase` that the fact is
 | 
			
		||||
  now true. For example, if it's rainy (and we've retracted the previous
 | 
			
		||||
  'sky is blue' fact), we might tell the `KnowledgeBase` that
 | 
			
		||||
  `('is', 'sky', 'grey')`.
 | 
			
		||||
+ A `KnowledgeBase` can also perform substitutions.
 | 
			
		||||
+ An action contains positive and negative preconditions, retractions,
 | 
			
		||||
  and updates. The positive condition list contains facts that must
 | 
			
		||||
  be true for a knowledge base, and the negative condition list contains
 | 
			
		||||
  facts that must be false. If these preconditions hold, the retractions
 | 
			
		||||
  are applied, followed by the updates.
 | 
			
		||||
+ See `test_actions.py` for an example.
 | 
			
		||||
 | 
			
		||||
### Limitations
 | 
			
		||||
 | 
			
		||||
+ Singleton facts aren't supported; that is, there is no way to make a
 | 
			
		||||
  `KnowledgeBase` assert that there is only one relationship → subject
 | 
			
		||||
  mapping. For example, the `KnowledgeBase` will admit that
 | 
			
		||||
  `('is', 'shrödingers cat', 'alive')` and
 | 
			
		||||
  `('is', 'schrödingers cat', 'dead') are both true simultaneously.
 | 
			
		||||
 | 
			
		||||
### TODO
 | 
			
		||||
 | 
			
		||||
+ Rewrite in C++?
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import copy
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
class Action:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, pos_precond, neg_precond, retracts, updates):
 | 
			
		||||
        self.pos_precond = copy.deepcopy(pos_precond)
 | 
			
		||||
        self.neg_precond = copy.deepcopy(neg_precond)
 | 
			
		||||
        self.retracts = copy.deepcopy(retracts)
 | 
			
		||||
        self.updates = copy.deepcopy(updates)
 | 
			
		||||
 | 
			
		||||
    def satisfied(self, kb, subject, obj):
 | 
			
		||||
        for fact in self.pos_precond:
 | 
			
		||||
            if not kb.ask(kb.subst(fact, subject, obj)):
 | 
			
		||||
                logging.warning('{} is not valid in the current knowledgebase'.format(fact))
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
        for fact in self.neg_precond:
 | 
			
		||||
            if kb.ask(kb.subst(fact, subject, obj)):
 | 
			
		||||
                logging.warning('{} is valid in the current knowledgebase'.format(fact))
 | 
			
		||||
                return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def perform(self, kb, subject, obj):
 | 
			
		||||
        if not self.satisfied(kb, subject, obj):
 | 
			
		||||
            return None
 | 
			
		||||
        kbprime = copy.deepcopy(kb)
 | 
			
		||||
        for retraction in self.retracts:
 | 
			
		||||
            kbprime.retract(kb.subst(retraction, subject, obj))
 | 
			
		||||
        for update in self.updates:
 | 
			
		||||
            kbprime.tell(kb.subst(update, subject, obj))
 | 
			
		||||
        return kbprime
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,159 @@
 | 
			
		|||
"""
 | 
			
		||||
AQE: A Query Engine
 | 
			
		||||
 | 
			
		||||
This is a proof of concept of a baseline query engine for AI work.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
class InvalidQuery(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
class Inconsistency(Exception):
 | 
			
		||||
    def __init__(self, fact):
 | 
			
		||||
        self.fact = fact
 | 
			
		||||
    
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return 'Inconsistency: {}'.format(self.fact)
 | 
			
		||||
 | 
			
		||||
class KnowledgeBase:
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        # TODO(kyle): support loading an initial set of facts.
 | 
			
		||||
        self.__kb__ = {}
 | 
			
		||||
        self.__facts__ = set()
 | 
			
		||||
        
 | 
			
		||||
    def tell(self, fact):
 | 
			
		||||
        relationship, subject, obj = fact
 | 
			
		||||
        
 | 
			
		||||
        # NB: in the future, these assertions may not need to be true; there
 | 
			
		||||
        # might be space in the world for "fuzzy" facts.
 | 
			
		||||
        assert(relationship)
 | 
			
		||||
        assert(subject)
 | 
			
		||||
        assert(obj)
 | 
			
		||||
        if relationship not in self.__kb__:
 | 
			
		||||
            self.__kb__[relationship] = {'subjects':{}, 'objects': {}}
 | 
			
		||||
        
 | 
			
		||||
        if subject not in self.__kb__[relationship]['subjects']:
 | 
			
		||||
            self.__kb__[relationship]['subjects'][subject] = set()
 | 
			
		||||
        self.__kb__[relationship]['subjects'][subject].add(obj)
 | 
			
		||||
        
 | 
			
		||||
        if obj not in self.__kb__[relationship]['objects']:
 | 
			
		||||
            self.__kb__[relationship]['objects'][obj] = set()
 | 
			
		||||
        self.__kb__[relationship]['objects'][obj].add(subject)
 | 
			
		||||
        self.__facts__.add(fact)
 | 
			
		||||
 | 
			
		||||
    def retract(self, fact):
 | 
			
		||||
        relationship, subject, obj = fact
 | 
			
		||||
        
 | 
			
		||||
        # For now, these assertions are required. In the future, it would be
 | 
			
		||||
        # interesting to say something to the effect of "forget everything you
 | 
			
		||||
        # know about X".
 | 
			
		||||
        assert(relationship)
 | 
			
		||||
        assert(subject)
 | 
			
		||||
        assert(obj)
 | 
			
		||||
        
 | 
			
		||||
        # TODO(kyle): answer existential question: if I delete all the objects
 | 
			
		||||
        # from a subject (or vice versa), should that subject/object be kept or
 | 
			
		||||
        # removed entirely? This is the difference between "I have no concept
 | 
			
		||||
        # of X" and "I am aware that X exists but I don't know anything about it".
 | 
			
		||||
        # For now, I'm electing to keep the entry.
 | 
			
		||||
        #
 | 
			
		||||
        # Similarly, if the relationship is empty, we could make the argument
 | 
			
		||||
        # for removing it --- at the expense of now saying that we have no
 | 
			
		||||
        # concept of this relationship.
 | 
			
		||||
        try:
 | 
			
		||||
            self.__kb__[relationship]['subjects'][subject].remove(obj)
 | 
			
		||||
            self.__kb__[relationship]['objects'][obj].remove(subject)
 | 
			
		||||
            self.__facts__.remove(fact)
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            # Being told to forget something about something you don't know
 | 
			
		||||
            # isn't an error.
 | 
			
		||||
            pass
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def ask(self, fact):
 | 
			
		||||
        relationship, subject, obj = fact
 | 
			
		||||
        
 | 
			
		||||
        # A future milestone will remove this requirement to support free
 | 
			
		||||
        # variables.
 | 
			
		||||
        assert(relationship)
 | 
			
		||||
        
 | 
			
		||||
        if relationship and subject and obj:
 | 
			
		||||
            if fact in self.__facts__:
 | 
			
		||||
                return [fact,]
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        if relationship and subject:
 | 
			
		||||
            return [(relationship, subject, _obj) for _obj
 | 
			
		||||
                    in self.__kb__[relationship]['subjects'][subject]]
 | 
			
		||||
 | 
			
		||||
        if relationship and obj:
 | 
			
		||||
            return [(relationship, _subject, obj) for _subject
 | 
			
		||||
                    in self.__kb__[relationship]['objects'][obj]]
 | 
			
		||||
 | 
			
		||||
    def facts(self):
 | 
			
		||||
        return list(self.__facts__)
 | 
			
		||||
        
 | 
			
		||||
    def is_consistent(self):
 | 
			
		||||
        try:
 | 
			
		||||
            for fact in self.__facts__:
 | 
			
		||||
                relationship, subject, obj = fact
 | 
			
		||||
                if obj not in self.__kb__[relationship]['subjects'][subject]:
 | 
			
		||||
                    raise Inconsistency(fact)
 | 
			
		||||
                if subject not in self.__kb__[relationship]['objects'][obj]:
 | 
			
		||||
                    raise Inconsistency(fact)
 | 
			
		||||
 | 
			
		||||
            for relationship, v in self.__kb__.items():
 | 
			
		||||
                for subject in v['subjects'].keys():
 | 
			
		||||
                    for obj in v['subjects'][subject]:
 | 
			
		||||
                        if (relationship, subject, obj) not in self.__facts__:
 | 
			
		||||
                            raise Inconsistency(fact)
 | 
			
		||||
 | 
			
		||||
                for obj in v['objects'].keys():
 | 
			
		||||
                    for subject in v['objects'][obj]:
 | 
			
		||||
                        if (relationship, subject, obj) not in self.__facts__:
 | 
			
		||||
                            raise Inconsistency(fact)
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            raise Inconsistency(fact)
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def __len__(self):
 | 
			
		||||
        return len(self.__facts__)
 | 
			
		||||
 | 
			
		||||
    def subst(self, fact, subject, obj):
 | 
			
		||||
        relationship, _subject, _obj = fact
 | 
			
		||||
        if _subject is None:
 | 
			
		||||
            _subject = subject
 | 
			
		||||
        if _subject == '?any':
 | 
			
		||||
            _subject = None
 | 
			
		||||
        elif _subject == '?subject':
 | 
			
		||||
            _subject = subject
 | 
			
		||||
        elif _subject == '?object':
 | 
			
		||||
            _subject = obj
 | 
			
		||||
 | 
			
		||||
        if _obj is None:
 | 
			
		||||
            _obj = obj
 | 
			
		||||
        if _obj == '?any':
 | 
			
		||||
            _obj = None
 | 
			
		||||
        elif _obj == '?subject':
 | 
			
		||||
            _obj = subject
 | 
			
		||||
        elif _obj == '?object':
 | 
			
		||||
            _obj = obj
 | 
			
		||||
 | 
			
		||||
        if _subject == '?current':
 | 
			
		||||
            possibilities = self.ask((relationship, None, _obj))
 | 
			
		||||
            assert(len(possibilities) == 1)
 | 
			
		||||
            _, _subject, _ = possibilities[0]
 | 
			
		||||
        elif _obj == '?current':
 | 
			
		||||
            possibilities = self.ask((relationship, subject, None))
 | 
			
		||||
            assert(len(possibilities) == 1)
 | 
			
		||||
            _, _, _obj = possibilities[0]
 | 
			
		||||
 | 
			
		||||
        return (relationship, _subject, _obj)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def from_facts(facts):
 | 
			
		||||
    kb = KnowledgeBase()
 | 
			
		||||
    for fact in facts:
 | 
			
		||||
        kb.tell(fact)
 | 
			
		||||
    return kb
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
import base64
 | 
			
		||||
import itertools
 | 
			
		||||
import json
 | 
			
		||||
import kb
 | 
			
		||||
import pickle
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
FACTS = """
 | 
			
		||||
gANdcQAoWAIAAABpc3EBWAgAAABhaXJsaW5lcnECWAUAAABGbHllcnEDh3EEaAFYBwAAAG9ha2xh
 | 
			
		||||
bmRxBVgHAAAAQWlycG9ydHEGh3EHaAFoBVgEAAAAQ2l0eXEIh3EJaAFYBgAAAGRlbnZlcnEKaAaH
 | 
			
		||||
cQtoAWgKaAiHcQxoAVgGAAAAY2JyNjAwcQ1YBgAAAERyaXZlcnEOh3EPaAFYBwAAAHRyb29wZXJx
 | 
			
		||||
EGgOh3ERWAIAAABhdHESaAJoCodxE2gSaA1oBYdxFGUu
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
def load():
 | 
			
		||||
    facts = base64.decodebytes(FACTS.encode('ascii'))
 | 
			
		||||
    facts = pickle.loads(facts)
 | 
			
		||||
    skb = kb.KnowledgeBase()
 | 
			
		||||
    for fact in facts:
 | 
			
		||||
        skb.tell(fact)
 | 
			
		||||
        
 | 
			
		||||
    return skb
 | 
			
		||||
 | 
			
		||||
def load_facts(corpus_path='data/corpus.json', is_count=1000000):
 | 
			
		||||
    facts = set()
 | 
			
		||||
    corpus = json.loads(open(corpus_path).read())
 | 
			
		||||
    if 'nouns' in corpus and 'adjectives' in corpus:
 | 
			
		||||
        perms = list(itertools.product(corpus['nouns'],
 | 
			
		||||
            corpus['adjectives']))
 | 
			
		||||
        if len(perms) < is_count:
 | 
			
		||||
            is_count = len(perms)-1;
 | 
			
		||||
        pool = random.choices(perms, k=is_count)
 | 
			
		||||
        for noun, adjective in pool:
 | 
			
		||||
            facts.add(('is', noun, adjective))
 | 
			
		||||
 | 
			
		||||
    if 'cities' in corpus:
 | 
			
		||||
        for city in corpus['cities']:
 | 
			
		||||
            facts.add(('is', city, 'City'))
 | 
			
		||||
 | 
			
		||||
    return facts
 | 
			
		||||
 | 
			
		||||
def generate_tail_number():
 | 
			
		||||
    letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
 | 
			
		||||
    tailno = 'N' + str(random.randint(10, 99))
 | 
			
		||||
    tailno += random.choice(letters)
 | 
			
		||||
    tailno += random.choice(letters)
 | 
			
		||||
    return tailno
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
import actions
 | 
			
		||||
import kb
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
INITIAL_FACTS = [
 | 
			
		||||
    ('is', 'N29EO', 'Plane'),
 | 
			
		||||
    ('at', 'N29EO', 'dia'),
 | 
			
		||||
    ('is', 'N10IV', 'Plane'),
 | 
			
		||||
    ('at', 'N10IV', 'oak'),
 | 
			
		||||
    ('is', 'N33FR', 'Plane'),
 | 
			
		||||
    ('at', 'N33FR', 'lga'),
 | 
			
		||||
    ('is', '1Z12345E0205271688', 'Package'),
 | 
			
		||||
    ('at', '1Z12345E0205271688', 'dia'),
 | 
			
		||||
    ('is', '1Z12345E6605272234', 'Package'),
 | 
			
		||||
    ('at', '1Z12345E6605272234', 'dia'),
 | 
			
		||||
    ('is', '1Z12345E0305271640', 'Package'),
 | 
			
		||||
    ('at', '1Z12345E0305271640', 'oak'),
 | 
			
		||||
    ('is', '1Z12345E1305277940', 'Package'),
 | 
			
		||||
    ('at', '1Z12345E1305277940', 'lga'),
 | 
			
		||||
    ('is', '1Z12345E6205277936', 'Package'),
 | 
			
		||||
    ('at', '1Z12345E6205277936', 'lga'),
 | 
			
		||||
    ('is', 'dia', 'Airport'),
 | 
			
		||||
    ('is', 'lga', 'Airport'),
 | 
			
		||||
    ('is', 'oak', 'Airport'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FLY_POS_PRECONDS = [
 | 
			
		||||
    ('is', '?subject', 'Plane'),
 | 
			
		||||
    ('is', '?object', 'Airport'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FLY_NEG_PRECONDS = [
 | 
			
		||||
    ('at', '?subject', '?object'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FLY_RETRACTIONS = [
 | 
			
		||||
    ('at', '?subject', '?current'), 
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FLY_UPDATES = [
 | 
			
		||||
    ('at', '?subject', '?object'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
fly = actions.Action(FLY_POS_PRECONDS, FLY_NEG_PRECONDS,
 | 
			
		||||
                     FLY_RETRACTIONS, FLY_UPDATES)
 | 
			
		||||
 | 
			
		||||
class ActionTestSuite(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.kb = kb.from_facts(INITIAL_FACTS)
 | 
			
		||||
 | 
			
		||||
    def test_a_flight(self):
 | 
			
		||||
        self.assertTrue(self.kb.ask(('at', 'N10IV', 'oak')))
 | 
			
		||||
        self.assertFalse(self.kb.ask(('at', 'N10IV', 'lga')))
 | 
			
		||||
 | 
			
		||||
        shadow = fly.perform(self.kb, 'N10IV', 'lga')
 | 
			
		||||
        self.assertTrue(shadow)
 | 
			
		||||
 | 
			
		||||
        # Shadow should reflect the updates and retractions.
 | 
			
		||||
        self.assertTrue(shadow.ask(('at', 'N10IV', 'lga')))
 | 
			
		||||
        self.assertFalse(shadow.ask(('at', 'N10IV', 'oak')))
 | 
			
		||||
 | 
			
		||||
        # The original shouldn't be touched.
 | 
			
		||||
        self.assertTrue(self.kb.ask(('at', 'N10IV', 'oak')))
 | 
			
		||||
        self.assertFalse(self.kb.ask(('at', 'N10IV', 'lga')))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
import copy
 | 
			
		||||
import kb
 | 
			
		||||
import random
 | 
			
		||||
import sample
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KnowledgeBaseTestSuite(unittest.TestCase):
 | 
			
		||||
    
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.kb = sample.load()
 | 
			
		||||
        
 | 
			
		||||
    def test_a_sanity_check(self):
 | 
			
		||||
        assert(self.kb.is_consistent())
 | 
			
		||||
        for fact in self.kb.__facts__:
 | 
			
		||||
            self.assertTrue(self.kb.ask(fact))
 | 
			
		||||
        
 | 
			
		||||
    def test_tell(self):
 | 
			
		||||
        new_fact = ('is', 'berkeley', 'City')
 | 
			
		||||
        
 | 
			
		||||
        # make sure it's not something we already know
 | 
			
		||||
        self.assertFalse(self.kb.ask(new_fact))
 | 
			
		||||
        self.kb.tell(new_fact)
 | 
			
		||||
        answer = self.kb.ask(new_fact)
 | 
			
		||||
        self.assertListEqual(answer, [new_fact,])
 | 
			
		||||
        
 | 
			
		||||
    def test_inconsistency(self):
 | 
			
		||||
        badkb = copy.deepcopy(self.kb)
 | 
			
		||||
        badfact = random.choice(badkb.facts())
 | 
			
		||||
        relationship, subject, obj = badfact
 | 
			
		||||
        
 | 
			
		||||
        # muck with subjects part
 | 
			
		||||
        badkb.__kb__[relationship]['subjects'][subject].remove(obj)
 | 
			
		||||
        with self.assertRaises(kb.Inconsistency):
 | 
			
		||||
            badkb.is_consistent()
 | 
			
		||||
        
 | 
			
		||||
        # muck with objects part
 | 
			
		||||
        badkb = copy.deepcopy(self.kb)
 | 
			
		||||
        badkb.__kb__[relationship]['objects'][obj].remove(subject)
 | 
			
		||||
        with self.assertRaises(kb.Inconsistency):
 | 
			
		||||
            badkb.is_consistent()
 | 
			
		||||
        
 | 
			
		||||
        # muck with facts part
 | 
			
		||||
        badkb = copy.deepcopy(self.kb)
 | 
			
		||||
        badkb.__facts__.remove(badfact)
 | 
			
		||||
        with self.assertRaises(kb.Inconsistency):
 | 
			
		||||
            badkb.is_consistent()
 | 
			
		||||
            
 | 
			
		||||
        # inject false data into the subject
 | 
			
		||||
        badkb = copy.deepcopy(self.kb)
 | 
			
		||||
        badkb.__kb__[relationship]['subjects'][subject].add('false memory')
 | 
			
		||||
        with self.assertRaises(kb.Inconsistency):
 | 
			
		||||
            badkb.is_consistent()
 | 
			
		||||
 | 
			
		||||
        # inject false data into the object
 | 
			
		||||
        badkb = copy.deepcopy(self.kb)
 | 
			
		||||
        badkb.__kb__[relationship]['objects'][obj].add('false memory')
 | 
			
		||||
        with self.assertRaises(kb.Inconsistency):
 | 
			
		||||
            badkb.is_consistent()
 | 
			
		||||
		Loading…
	
		Reference in New Issue