/******************************************************************************
*
* Copyright 2014 Paphus Solutions Inc.
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package org.botlibre.thought.forgetfulness;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import org.botlibre.api.knowledge.Network;
import org.botlibre.api.knowledge.Relationship;
import org.botlibre.api.knowledge.Vertex;
import org.botlibre.knowledge.BasicVertex;
import org.botlibre.knowledge.Primitive;
import org.botlibre.thought.BasicThought;
import org.botlibre.util.Utils;
import org.eclipse.persistence.internal.helper.IdentityHashSet;
/**
* Forgetfulness is a sub-conscious thought that cleans up the memory to remove unused vertices and reduce relationship size.
*/
public class Forgetfulness extends BasicThought {
public static int PAGE = 5000;
/** Min number of days to keep conversation and context data for. */
public static long EXPIRY = 7;
public static int MAX_SIZE = 100000;
public static int MAX_RELATIONSHIPS = 150;
public static long TIME_TO_LIVE = (10 * Utils.MINUTE);
/** Min number of days to keep conversation and context data for. */
public long expiry = EXPIRY;
public int maxSize = MAX_SIZE;
public int maxRelationships = MAX_RELATIONSHIPS;
public enum ForgetType { Unreferenced, UnreferencedData, OldConversations, LeastReferenced, UnreferencedPinned, Grammar, FixResponses, FixRelationships }
public Forgetfulness() { }
/**
* Analyse the active memory.
* Output the active article to the senses.
*/
@Override
public void think() {
try {
if (!this.bot.mind().isConscious()) {
return;
}
if (this.isStopped) {
return;
}
if (this.isEnabled) {
// Only count 1 in 20, to help concurrency.
if (Utils.random().nextInt(20) >= 19) {
Network memory = this.bot.memory().newMemory();
Vertex forgetfulness = memory.createVertex(getPrimitive());
Vertex activeCount = forgetfulness.getRelationship(Primitive.COUNT);
int accessCount = 0;
if (activeCount == null) {
forgetfulness.addRelationship(Primitive.COUNT, memory.createVertex(accessCount));
} else {
accessCount = ((Number)activeCount.getData()).intValue();
}
accessCount = accessCount + 20;
memory.save();
log("accessCount", Level.FINE, accessCount);
if (accessCount < (this.maxRelationships * 2)) {
forgetfulness.setRelationship(Primitive.COUNT, memory.createVertex(accessCount));
memory.save();
return;
} else {
forgetfulness.setRelationship(Primitive.COUNT, memory.createVertex(0));
}
memory.save();
forget(memory, false);
}
}
} catch (Exception failure) {
log(failure);
}
}
public int forget(ForgetType type, int numberToDelete, Network memory) throws Exception {
return forget(type, numberToDelete, TIME_TO_LIVE, memory);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public int forget(ForgetType type, int numberToDelete, long timeToLive, Network memory) throws Exception {
if (!this.bot.mind().isConscious()) {
return 0;
}
int errors = 0;
boolean found = true;
boolean first = true;
long start = System.currentTimeMillis();
while (found && (numberToDelete > 0) && this.bot.mind().isConscious()) {
if (this.isStopped) {
break;
}
long batchStart = System.currentTimeMillis();
List<Vertex> unreferenced = new ArrayList<Vertex>();
if (type == ForgetType.Unreferenced) {
log("Searching for unreferenced vertices with no data", Level.INFO);
unreferenced = memory.findByNativeQuery(
"SELECT v.* FROM VERTEX v LEFT OUTER JOIN RELATIONSHIP r ON (((r.TARGET_ID = v.ID) OR (r.TYPE_ID = v.ID)) OR (r.META_ID = v.ID)) WHERE r.ID IS NULL and ((v.PINNED = false) AND (v.DATAVALUE IS NULL)) LIMIT " + PAGE,
BasicVertex.class, PAGE);
//int rowCount = memory.executeNativeQuery(
// "DELETE FROM VERTEX v WHERE v.ID IN (" +
// "SELECT v.ID FROM VERTEX v LEFT OUTER JOIN RELATIONSHIP r ON (((r.TARGET_ID = v.ID) OR (r.TYPE_ID = v.ID)) OR (r.META_ID = v.ID)) WHERE r.ID IS NULL and ((v.PINNED = false) AND (v.DATAVALUE IS NULL)))");
log("Unreferenced verticies with no data query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Removing unreferenced vertices with no data", Level.WARNING, unreferenced.size());
} else if (type == ForgetType.UnreferencedData) {
log("Searching for unreferenced vertices", Level.INFO);
unreferenced = memory.findByNativeQuery(
"SELECT v.* FROM VERTEX v LEFT OUTER JOIN RELATIONSHIP r ON (((r.TARGET_ID = v.ID) OR (r.TYPE_ID = v.ID)) OR (r.META_ID = v.ID)) WHERE r.ID IS NULL and ((v.PINNED = false) AND (v.DATATYPE <> 'Primitive')) LIMIT " + PAGE,
BasicVertex.class, PAGE);
//int rowCount = memory.executeNativeQuery(
// "DELETE FROM VERTEX v WHERE v.ID IN (" +
// "SELECT v.ID FROM VERTEX v LEFT OUTER JOIN RELATIONSHIP r ON (((r.TARGET_ID = v.ID) OR (r.TYPE_ID = v.ID)) OR (r.META_ID = v.ID)) WHERE r.ID IS NULL and ((v.PINNED = false) AND (v.DATATYPE <> 'Primitive')) LIMIT " + numberToDelete + ")");
log("Unreferenced vertices query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Removing unreferenced vertices", Level.WARNING, unreferenced.size());
} else if (type == ForgetType.OldConversations) {
log("Searching for old conversation vertices", Level.INFO);
Vertex instantiation = memory.createVertex(Primitive.INSTANTIATION).detach();
Vertex context = memory.createVertex(Primitive.CONTEXT).detach();
Vertex conversation = memory.createVertex(Primitive.CONVERSATION).detach();
Vertex input = memory.createVertex(Primitive.INPUT).detach();
java.sql.Date date = new java.sql.Date(System.currentTimeMillis() - (Utils.DAY * this.expiry));
Map parameters = new HashMap();
parameters.put("type", instantiation);
parameters.put("context", context);
parameters.put("conversation", conversation);
parameters.put("input", input);
parameters.put("date", date);
unreferenced = memory.findAllQuery(
"Select v FROM Vertex v join v.allRelationships r where r.type = :type and v.creationDate < :date and (r.target = :context or r.target = :conversation or r.target = :input) "
+ "order by v.creationDate", parameters, PAGE, 0);
log("Old conversation query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Removing old conversation vertices", Level.WARNING, unreferenced.size());
// Check if still too many conversations and delete more.
if (unreferenced.isEmpty()) {
parameters = new HashMap();
parameters.put("type", instantiation);
parameters.put("context", context);
parameters.put("conversation", conversation);
parameters.put("input", input);
unreferenced = memory.findAllQuery(
"Select v FROM Vertex v join v.allRelationships r where r.type = :type and (r.target = :context or r.target = :conversation or r.target = :input) "
+ "order by v.creationDate", parameters, PAGE, 0);
log("Old conversation query time", Level.INFO, System.currentTimeMillis() - batchStart);
if (unreferenced.size() < PAGE) {
unreferenced = new ArrayList<Vertex>();
} else {
// Keep a minimum of a 1/2 page.
for (int index = 0; index < (PAGE / 2); index++) {
unreferenced.remove(unreferenced.size() - 1);
}
}
log("Removing old conversation vertices", Level.WARNING, unreferenced.size());
}
} else if (type == ForgetType.LeastReferenced) {
log("Searching for vertices with fewest references", Level.INFO, numberToDelete);
//List<Object[]> byReferences = memory.findAllQuery("Select count(v2) c, v from Vertex v, Vertex v2 join v2.allRelationships r2 "
// + "where v.pinned = false and (v.dataType is null or v.dataType <> 'Primitive') and (r2.target = v or r2.type = v or r2.meta = v) "
// + "group by v order by c, v.accessCount, v.accessDate", 5000);
unreferenced = memory.findByNativeQuery(
"SELECT v.* FROM VERTEX v JOIN RELATIONSHIP r ON (((r.TARGET_ID = v.ID) OR (r.TYPE_ID = v.ID)) OR (r.META_ID = v.ID)) "
+ "WHERE ((v.PINNED = false) AND ((v.DATATYPE IS NULL) OR ((v.DATATYPE <> 'Primitive') AND (v.DATATYPE <> 'Meta')))) "
+ "GROUP BY v.ID ORDER BY COUNT(r.SOURCE_ID), v.ACCESSCOUNT, v.ACCESSDATE LIMIT " + PAGE, BasicVertex.class, PAGE);
log("Fewest references query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Found vertices with fewest references", Level.INFO, unreferenced.size());
} else if (type == ForgetType.UnreferencedPinned) {
log("Searching for unreferenced pinned vertices", Level.INFO);
unreferenced = memory.findByNativeQuery(
"SELECT v.* FROM VERTEX v LEFT OUTER JOIN RELATIONSHIP r ON (((r.TARGET_ID = v.ID) OR (r.TYPE_ID = v.ID)) OR (r.META_ID = v.ID)) WHERE r.ID IS NULL and ((v.PINNED = true) AND (v.DATAVALUE IS NULL OR v.DATATYPE <> 'Primitive')) LIMIT " + PAGE,
BasicVertex.class, PAGE);
log("Unreferenced pinned verticies query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Removing unreferenced pinned vertices", Level.WARNING, unreferenced.size());
} else if (type == ForgetType.Grammar) {
log("Searching for grammar data", Level.INFO);
Vertex next = memory.createVertex(Primitive.NEXT);
Vertex previous = memory.createVertex(Primitive.PREVIOUS);
Vertex sentence = memory.createVertex(Primitive.SENTENCE);
Vertex instantiation = memory.createVertex(Primitive.INSTANTIATION);
Vertex word = memory.createVertex(Primitive.WORD);
unreferenced = memory.findByNativeQuery(
"SELECT DISTINCT v.* FROM VERTEX v JOIN RELATIONSHIP r ON (r.SOURCE_ID = v.ID) JOIN RELATIONSHIP r2 ON (r2.SOURCE_ID = v.ID) WHERE (r.TYPE_ID = "
+ next.getId() + " OR r.TYPE_ID = " + previous.getId() + " OR r.TYPE_ID = " + sentence.getId() + ") AND r2.TYPE_ID = "
+ instantiation.getId() + " AND r2.TARGET_ID = " + word.getId() + " LIMIT " + PAGE,
BasicVertex.class, PAGE);
if (first) {
unreferenced.add(memory.createVertex(Primitive.NULL));
}
log("Grammar verticies query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Removing grammar data", Level.WARNING, unreferenced.size());
} else if (type == ForgetType.FixResponses) {
log("Searching for response question with no word references", Level.INFO);
Vertex response = memory.createVertex(Primitive.RESPONSE);
Vertex sentence = memory.createVertex(Primitive.SENTENCE);
Vertex instantiation = memory.createVertex(Primitive.INSTANTIATION);
unreferenced = memory.findByNativeQuery(
"SELECT DISTINCT v.* FROM VERTEX v JOIN RELATIONSHIP r ON (r.SOURCE_ID = v.ID) JOIN RELATIONSHIP r2 ON (r2.SOURCE_ID = v.ID) WHERE (r.TYPE_ID = "
+ response.getId() + " AND r2.TYPE_ID = "
+ instantiation.getId() + " AND r2.TARGET_ID = " + sentence.getId() + ") LIMIT " + PAGE,
BasicVertex.class, PAGE);
log("Response verticies query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Fixing response data", Level.WARNING, unreferenced.size());
} else if (type == ForgetType.FixRelationships) {
log("Searching all relationships", Level.INFO);
unreferenced = memory.findByNativeQuery(
"SELECT v.* FROM VERTEX v ORDER BY random() LIMIT " + PAGE,
BasicVertex.class, PAGE);
log("All relationships query time", Level.INFO, System.currentTimeMillis() - batchStart);
log("Fixing relationships data", Level.WARNING, unreferenced.size());
}
found = unreferenced.size() > 0;
int failures = 0;
for (Vertex vertex : unreferenced) {
if (vertex == null) {
continue;
}
if (vertex.getCreationDate() != null && ((System.currentTimeMillis() - vertex.getCreationDate().getTime()) < timeToLive)) {
log("Ignoring new vertex", Level.FINER, vertex, numberToDelete);
numberToDelete--;
failures++;
continue;
}
log("Processing vertex", Level.FINER, vertex);
try {
if (this.isStopped) {
return 0;
}
if (type == ForgetType.Grammar) {
vertex.internalRemoveRelationships(Primitive.NEXT);
vertex.internalRemoveRelationships(Primitive.PREVIOUS);
vertex.internalRemoveRelationships(Primitive.SENTENCE);
} else if (type == ForgetType.FixRelationships) {
for (Iterator<Relationship> iterator = vertex.allRelationships(); iterator.hasNext(); ) {
Relationship relationship = iterator.next();
if (relationship.checkHashCode()) {
log("Fixing relationship hashcode", Level.INFO, relationship);
}
}
} else if (type == ForgetType.FixResponses) {
if (!vertex.instanceOf(Primitive.PATTERN) && vertex.hasRelationship(Primitive.RESPONSE)) {
// Associate each word in each question with the question.
Collection<Relationship> words = vertex.getRelationships(Primitive.WORD);
if (words != null) {
boolean missing = false;
for (Relationship word : words) {
if (! word.getTarget().hasRelationship(Primitive.QUESTION, vertex)) {
word.getTarget().addRelationship(Primitive.QUESTION, vertex);
if (!missing) {
log("Fixing response", Level.INFO, vertex);
}
missing = true;
}
}
}
}
} else if (type == ForgetType.OldConversations) {
memory.removeVertex(vertex);
//memory.removeVertexAndReferences(vertex);
} else {
memory.removeVertex(vertex);
}
} catch (Exception failure) {
errors++;
failures++;
if (errors > 5) {
throw failure;
}
log(failure);
}
numberToDelete--;
if (numberToDelete <= 0) {
break;
}
}
if (failures == unreferenced.size()) {
found = false;
}
if (this.isStopped) {
return 0;
}
try {
memory.save();
memory.clear();
} catch (Exception failure) {
errors++;
if (errors > 5) {
throw failure;
}
log(failure);
}
log("Processing batch time", Level.INFO, System.currentTimeMillis() - batchStart);
first = false;
if (type == ForgetType.FixRelationships || type == ForgetType.FixResponses) {
break;
}
}
log("Processing total time", Level.INFO, System.currentTimeMillis() - start);
return numberToDelete;
}
public void forget(Network memory) throws Exception {
forget(memory, true);
}
public void forget(Network memory, boolean force) throws Exception {
forget(memory, force, TIME_TO_LIVE);
}
public void forget(Network memory, boolean force, long timeToLive) throws Exception {
if (this.isStopped) {
return;
}
forgetRelationships(memory);
if (this.isStopped) {
return;
}
int count = ((Number)memory.findAllQuery("Select count(v) from Vertex v").get(0)).intValue();
// Only run if much bigger, otherwise let service handle it at night.
int max = this.maxSize;
if (!force) {
max = (int)(max * 1.5);
}
log("Current number of vetices (current, max, threshold)", Level.INFO, count, this.maxSize, max);
if (count > max) {
int numberToDelete = count - this.maxSize + (this.maxSize / 20);
log("Max number of vertices exceeded (max, current, deletions)", Level.WARNING, this.maxSize, count, numberToDelete);
numberToDelete = forget(ForgetType.Unreferenced, numberToDelete, timeToLive, memory);
if (numberToDelete == 0) {
return;
}
numberToDelete = forget(ForgetType.UnreferencedData, numberToDelete, timeToLive, memory);
if (numberToDelete == 0) {
return;
}
numberToDelete = forget(ForgetType.OldConversations, numberToDelete, timeToLive, memory);
if (numberToDelete == 0) {
return;
}
// May have more unreferenced data.
numberToDelete = forget(ForgetType.Unreferenced, numberToDelete, timeToLive, memory);
if (numberToDelete == 0) {
return;
}
numberToDelete = forget(ForgetType.UnreferencedData, numberToDelete, timeToLive, memory);
if (numberToDelete == 0) {
return;
}
// Record potential destructive forget.
memory.createVertex(getPrimitive()).setRelationship(Primitive.LAST, memory.createTimestamp());
memory.save();
numberToDelete = forget(ForgetType.LeastReferenced, numberToDelete, timeToLive, memory);
memory.clear();
this.bot.memory().freeMemory();
}
}
@SuppressWarnings("unchecked")
public void forgetRelationships(Network memory) throws Exception {
if (this.isStopped) {
return;
}
long start = System.currentTimeMillis();
List<Vertex> tooManyReferences = memory.findAllQuery("Select v from Vertex v where v.dirty = true");
//List<Vertex> tooManyReferences = memory.findAllQuery("Select v from Relationship r join r.source v group by r.type, v having count(r) > " + MAX_RELATIONSHIPS);
//List<Vertex> tooManyReferences = memory.findByNativeQuery(
// "SELECT t0.* FROM VERTEX t0, RELATIONSHIP t1 WHERE (t0.ID = t1.SOURCE_ID) AND (t1.PINNED = false) GROUP BY t0.ID, t1.TYPE_ID HAVING (COUNT(t1.TYPE_ID) > " + this.maxRelationships + ") LIMIT " + PAGE,
// BasicVertex.class, PAGE);
log("Max relationships check query time", Level.FINE, System.currentTimeMillis() - start);
if ((System.currentTimeMillis() - start) > 5000) {
log("Max relationships check query time", Level.WARNING, System.currentTimeMillis() - start);
}
int errors = 0;
if (tooManyReferences.size() > 0) {
log("Veticies exceeding max number of relationships detected", Level.INFO, this.maxRelationships, tooManyReferences.size());
for (Vertex vertex : tooManyReferences) {
if (this.isStopped) {
break;
}
log("Vertex has too many relationships", Level.FINER, vertex);
// Check for corruption.
if (vertex.getAllRelationships().size() != vertex.totalRelationships()) {
log("Vertex has corrupt relationships", Level.FINER, vertex, vertex.getAllRelationships().size(), vertex.totalRelationships());
Set<Relationship> valid = new IdentityHashSet();
for (Iterator<Relationship> iterator = vertex.allRelationships(); iterator.hasNext(); ) {
valid.add(iterator.next());
}
for (Relationship relationship : new ArrayList<Relationship>(vertex.getAllRelationships())) {
if (!valid.contains(relationship)) {
log("Removing corrupt relationship", Level.FINER, relationship);
vertex.internalRemoveRelationship(relationship);
}
}
}
// Check hashcodes
for (Relationship relationship : new ArrayList<Relationship>(vertex.getAllRelationships())) {
if (relationship.checkHashCode()) {
log("Fixing relationship hashcode", Level.FINER, relationship);
}
}
// Remove all references.
for (Entry<Vertex, Map<Relationship, Relationship>> entry : vertex.getRelationships().entrySet()) {
log("Relationship size", Level.FINER, entry.getKey(), entry.getValue().size());
if (entry.getValue().size() > this.maxRelationships) {
int numberToDelete = entry.getValue().size() - this.maxRelationships;
log("Removing vertex relationships exceeding max size for type", Level.FINER, vertex, entry.getKey(), numberToDelete);
List<Relationship> sorted = new ArrayList<Relationship>(entry.getValue().values());
Collections.sort(sorted, new Comparator<Relationship>() {
public int compare(Relationship one, Relationship two) {
if (one.isPinned() && !two.isPinned()) {
return 1;
}
if (!one.isPinned() && two.isPinned()) {
return -1;
}
if (one.getCorrectness() == two.getCorrectness()) {
if (one.getAccessCount() == two.getAccessCount()) {
if (one.getAccessDate() == null && two.getAccessDate() == null) {
return 0;
} else if (two.getAccessDate() == null) {
return 1;
} else if (one.getAccessDate() == null) {
return -1;
} else if (one.getAccessDate().getTime() > two.getAccessDate().getTime()) {
return 1;
} else if (one.getAccessDate().getTime() == two.getAccessDate().getTime()) {
return 0;
}
return -1;
} else {
if (one.getAccessCount() > two.getAccessCount()) {
return 1;
}
return -1;
}
} else {
if (one.getCorrectness() > two.getCorrectness()) {
return 1;
}
return -1;
}
}
});
for (int index = 0; index < numberToDelete; index++) {
if (this.isStopped) {
break;
}
Relationship toBeDeleted = sorted.get(index);
if (!toBeDeleted.isPinned()) {
log("Removing relationship with least correctness", Level.FINER, toBeDeleted);
toBeDeleted.getSource().internalRemoveRelationship(toBeDeleted);
}
}
// Fix indexes.
// TODO: Remove for now, seems to bottleneck db.
//vertex.fixRelationships(entry.getKey());
}
}
vertex.setIsDirty(false);
}
if (this.isStopped) {
return;
}
try {
memory.save();
memory.clear();
} catch (Exception failure) {
errors++;
if (errors > 5) {
throw failure;
}
log(failure);
}
log("Max relationships total time", Level.INFO, System.currentTimeMillis() - start);
}
}
/**
* Thoughts can be conscious or sub-conscious.
* A conscious thought is run by the mind single threaded with exclusive access to the short term memory.
* A sub-conscious thought is run concurrently, and must run in its own memory space.
*/
@Override
public boolean isConscious() {
return false;
}
/**
* Return if this thought must run even under stress.
*/
@Override
public boolean isCritical() {
return true;
}
public long getExpiry() {
return expiry;
}
public void setExpiry(long expiry) {
this.expiry = expiry;
}
public int getMaxSize() {
return maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public int getMaxRelationships() {
return maxRelationships;
}
public void setMaxRelationships(int maxRelationships) {
this.maxRelationships = maxRelationships;
}
}