initial pass at first query parsing
All checks were successful
studiorailgun/trpg/pipeline/head This commit looks good

This commit is contained in:
austin 2024-12-29 20:52:42 -05:00
parent 3781c73d88
commit ee5f4eab86
22 changed files with 696 additions and 37 deletions

View File

@ -8,8 +8,8 @@ sitting in a tavern by a fireplace
Respond to "What color is your hat?"
- determine sentence function
- determine what is being queried for
- respond with the correct information

31
data/webs/bert.json Normal file
View File

@ -0,0 +1,31 @@
{
"tag" : "bert",
"nodes" : {
"0" : {
"id" : 0,
"name" : "Bert"
},
"9" : {
"id" : 9,
"name" : "Bert's Hat"
}
},
"relations" : {
"0" : {
"id" : 0,
"name" : "nameOf",
"parent" : 0,
"child" : 2,
"childWeb" : "root"
},
"1" : {
"id" : 1,
"name" : "qualiaOf",
"parent" : 1,
"parentWeb" : "color",
"child" : 9
}
},
"children" : [
]
}

View File

@ -0,0 +1,24 @@
{
"tag" : "qualia",
"nodes" : {
"0" : {
"id" : 0,
"name" : "qualia"
}
},
"relations" : {
"0" : {
"id" : 0,
"name" : "instanceOf",
"parent" : 0,
"child" : 0,
"childWeb" : "color"
}
},
"anchors" : {
"qualia" : 0
},
"children" : [
"./data/webs/qualia/color.json"
]
}

View File

@ -1,11 +1,6 @@
{
"tag" : "root",
"selfId": 2,
"nodes" : {
"0" : {
"id" : 0,
"name" : "Bert"
},
"1" : {
"id" : 1,
"name" : "Name"
@ -37,53 +32,71 @@
"8" : {
"id" : 8,
"name" : "Hat"
},
"10" : {
"id" : 10,
"name" : "concept"
}
},
"relations" : {
"0" : {
"id" : 0,
"name" : "Name",
"parent" : 0,
"child" : 2
},
"1" : {
"id" : 1,
"name" : "InstanceOf",
"name" : "instanceOf",
"parent" : 3,
"child" : 2
},
"2" : {
"id" : 2,
"name" : "InstanceOf",
"name" : "instanceOf",
"parent" : 1,
"child" : 0
"child" : 0,
"childWeb" : "bert"
},
"3" : {
"id" : 3,
"name" : "InstanceOf",
"name" : "instanceOf",
"parent" : 3,
"child" : 4
},
"4" : {
"id" : 4,
"name" : "ParticipantOf",
"name" : "participantOf",
"parent" : 7,
"child" : 4
},
"5" : {
"id" : 5,
"name" : "ParticipantOf",
"name" : "participantOf",
"parent" : 7,
"child" : 2
},
"6" : {
"id" : 6,
"name" : "Possession",
"name" : "possessionOf",
"parent" : 2,
"child" : 9,
"childWeb" : "bert"
},
"7" : {
"id" : 7,
"name" : "instanceOf",
"parent" : 8,
"child" : 9,
"childWeb" : "bert"
},
"8" : {
"id" : 8,
"name" : "instanceOf",
"parent" : 10,
"child" : 8
}
},
"anchors" : {
"self" : 2,
"concept" : 10
},
"children" : [
"./data/webs/color.json"
"./data/webs/qualia/qualia.json",
"./data/webs/bert.json"
]
}

View File

@ -1,6 +1,5 @@
{
"tag" : "root",
"selfId": 2,
"children" : [
"./data/webs/root.json"
]

View File

@ -1,5 +1,7 @@
package org.studiorailgun.conversation.evaluators.goal;
import org.studiorailgun.conversation.evaluators.query.QueryData;
/**
* Data about the AI's goal in the conversation
*/
@ -21,6 +23,18 @@ public class GoalData {
*/
ConversationGoal goal;
/**
* The query data
*/
QueryData queryData;
/**
* The query data for the conversation
*/
public GoalData(){
this.queryData = new QueryData();
}
public ConversationGoal getGoal() {
return goal;
}
@ -29,6 +43,8 @@ public class GoalData {
this.goal = goal;
}
public QueryData getQueryData() {
return queryData;
}
}

View File

@ -1,5 +1,15 @@
package org.studiorailgun.conversation.evaluators.query;
import java.util.List;
import org.studiorailgun.Globals;
import org.studiorailgun.conversation.tracking.Conversation;
import org.studiorailgun.conversation.tracking.Quote;
import org.studiorailgun.knowledge.Node;
import org.studiorailgun.knowledge.query.InstanceQuery;
import org.studiorailgun.knowledge.query.QualiaQuery;
import org.studiorailgun.knowledge.query.filter.PossessionQueryFilter;
/**
* Interrogatives available
*/
@ -25,4 +35,60 @@ public class Interrogative {
WHEN,
}
/**
* Evaluates a query of "which"
* @param conversation The conversation
* @param quote The quote
* @param interrogative The interrogative noun
* @param items The items noun
* @return null
*/
public static void evalWhichQuery(Conversation conversation, Quote quote, NounStack interrogative, NounStack items){
if(QualiaQuery.isQualiaClarificationQuery(interrogative)){
Node finalQualifier = InstanceQuery.getConcept(items.indexedWord.originalText());
if(items.possessive != null){
List<Node> qualifierInstances = InstanceQuery.getInstances(finalQualifier);
finalQualifier = PossessionQueryFilter.withPossessor(qualifierInstances, Globals.web.getAnchors().getSelfNode());
}
List<Node> qualiaOfInstance = QualiaQuery.getQualiaOfInstance(finalQualifier);
Node qualiaType = QualiaQuery.getRequestedQualiaType(interrogative);
conversation.addQuote(quote);
conversation.getGoalData().getQueryData().getRecentQueries().add(quote);
} else {
throw new Error("Unknown query type!");
}
}
/**
* Evaluates a query of "what"
* @param conversation The conversation
* @param quote The quote
* @param interrogative The interrogative noun
* @param items The items noun
* @return null
*/
public static void evalWhatQuery(Conversation conversation, Quote quote, NounStack interrogative, NounStack items){
if(QualiaQuery.isQualiaClarificationQuery(interrogative)){
Node finalQualifier = InstanceQuery.getConcept(items.indexedWord.originalText());
if(items.possessive != null){
List<Node> qualifierInstances = InstanceQuery.getInstances(finalQualifier);
finalQualifier = PossessionQueryFilter.withPossessor(qualifierInstances, Globals.web.getAnchors().getSelfNode());
}
List<Node> qualiaOfInstance = QualiaQuery.getQualiaOfInstance(finalQualifier);
Node qualiaType = QualiaQuery.getRequestedQualiaType(interrogative);
conversation.addQuote(quote);
conversation.getGoalData().getQueryData().getRecentQueries().add(quote);
} else {
throw new Error("Unknown query type!");
}
}
}

View File

@ -77,4 +77,18 @@ public class NounStack {
return rVal;
}
public String getInterrogative() {
return interrogative;
}
public String getPossessive() {
return possessive;
}
public IndexedWord getIndexedWord() {
return indexedWord;
}
}

View File

@ -0,0 +1,34 @@
package org.studiorailgun.conversation.evaluators.query;
import java.util.LinkedList;
import java.util.List;
import org.studiorailgun.conversation.tracking.Quote;
/**
* Data about recent queries in the conversation
*/
public class QueryData {
/**
* The recent queries
*/
List<Quote> recentQueries = new LinkedList<Quote>();
/**
* Gets the recent queries in the conversation
* @return The recent queries
*/
public List<Quote> getRecentQueries(){
return recentQueries;
}
/**
* Adds the quote to the recent queries
* @param quote The quote
*/
public void addQuery(Quote quote){
recentQueries.add(quote);
}
}

View File

@ -1,12 +1,17 @@
package org.studiorailgun.conversation.evaluators.query;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.studiorailgun.Globals;
import org.studiorailgun.conversation.parser.NLPParser;
import org.studiorailgun.conversation.parser.PennTreebankTagSet;
import org.studiorailgun.conversation.tracking.Conversation;
import org.studiorailgun.conversation.tracking.Quote;
import org.studiorailgun.knowledge.Node;
import org.studiorailgun.knowledge.query.InstanceQuery;
import org.studiorailgun.knowledge.query.filter.PossessionQueryFilter;
import edu.stanford.nlp.ling.IndexedWord;
import edu.stanford.nlp.semgraph.SemanticGraph;
@ -57,13 +62,38 @@ public class QueryEval {
//get the two things we're comparing
SemanticGraph graph = quote.getGraph();
IndexedWord root = graph.getFirstRoot();
Set<IndexedWord> dependents = graph.descendants(root);
Set<IndexedWord> children = graph.getChildren(root);
Iterator<IndexedWord> iterator = children.iterator();
IndexedWord firstItem = iterator.next();
IndexedWord secondItem = iterator.next();
NounStack firstNoun = NounStack.parse(graph, firstItem);
NounStack secondNoun = NounStack.parse(graph, secondItem);
//interrogative error checking
if(firstNoun.interrogative != null && secondNoun.interrogative != null){
String message = "Comparing two interrogatives!";
throw new Error(message);
}
//handle interrogative solving
if(firstNoun.interrogative != null || secondNoun.interrogative != null){
NounStack interrogativeStack = firstNoun.interrogative != null ? firstNoun : secondNoun;
NounStack qualifierStack = firstNoun.interrogative != null ? secondNoun : firstNoun;
switch(interrogativeStack.interrogative.toLowerCase()){
case "which": {
Interrogative.evalWhichQuery(conversation, quote, interrogativeStack, qualifierStack);
} break;
case "what": {
Interrogative.evalWhatQuery(conversation, quote, interrogativeStack, qualifierStack);
} break;
default : {
throw new Error("Unhandled interrogative type! " + interrogativeStack.interrogative.toLowerCase());
}
}
System.out.println("Turn this into a query");
}
}
}

View File

@ -1,7 +1,11 @@
package org.studiorailgun.conversation.tracking;
import java.util.LinkedList;
import java.util.List;
import org.studiorailgun.conversation.evaluators.goal.GoalData;
import org.studiorailgun.conversation.evaluators.greet.GreetingData;
import org.studiorailgun.conversation.evaluators.query.QueryData;
import org.studiorailgun.knowledge.KnowledgeWeb;
import org.studiorailgun.knowledge.Node;
@ -35,6 +39,11 @@ public class Conversation {
*/
GoalData goalData;
/**
* The history of the conversation
*/
List<Quote> history;
/**
* Parses the current conversation's data from the knowledge web
* @param web The web
@ -44,7 +53,7 @@ public class Conversation {
Node node = web.getNode(7);
//find the self
ConvParticipant self = new ConvParticipant(web.getSelf());
ConvParticipant self = new ConvParticipant(web.getAnchors().getSelfNode());
//find the other participant
ConvParticipant other = new ConvParticipant(web.getNode(4));
@ -65,6 +74,7 @@ public class Conversation {
this.other = other;
this.greetingData = new GreetingData();
this.goalData = new GoalData();
this.history = new LinkedList<Quote>();
}
/**
@ -99,6 +109,22 @@ public class Conversation {
this.goalData = goalData;
}
/**
* Gets the history of the conversation
* @return The history of the conversation
*/
public List<Quote> getHistory(){
return history;
}
/**
* Adds a quote to the conversation
* @param quote The quote
*/
public void addQuote(Quote quote){
this.history.add(quote);
}
}

View File

@ -10,6 +10,7 @@ import java.util.Map.Entry;
import org.studiorailgun.FileUtils;
import org.studiorailgun.Globals;
import org.studiorailgun.knowledge.anchor.AnchorNodes;
/**
* A knowledge web
@ -38,9 +39,15 @@ public class KnowledgeWeb {
transient Map<String,List<Integer>> typeRelationLookup;
/**
* The node that represents the current ai
* Map of node -> relations where that node is the parent
*/
int selfId;
transient Map<Node,List<Relation>> parentRelations;
/**
* The anchor nodes
*/
AnchorNodes anchors;
/**
* Child webs
*/
@ -53,6 +60,7 @@ public class KnowledgeWeb {
this.nodes = new HashMap<Integer,Node>();
this.relations = new HashMap<Integer,Relation>();
this.typeRelationLookup = new HashMap<String,List<Integer>>();
this.parentRelations = new HashMap<Node,List<Relation>>();
}
/**
@ -70,7 +78,12 @@ public class KnowledgeWeb {
Globals.web = rVal;
//fix the IDs referenced by the links
for(Relation relation : rVal.relations.values()){
String lookupId = "node-" + relation.webTag + relation.parent;
String lookupId = "";
if(relation.parentWeb != null){
lookupId = "node-" + relation.parentWeb + relation.parent;
} else {
lookupId = "node-" + relation.webTag + relation.parent;
}
int nodeId = crossRelationLookupMap.get(lookupId);
relation.parent = nodeId;
@ -85,6 +98,20 @@ public class KnowledgeWeb {
}
relation.child = nodeId;
}
//populate lookup acceleration structures
for(Relation relation : rVal.relations.values()){
Node parent = relation.getParent();
if(rVal.parentRelations.containsKey(parent)){
List<Relation> relations = rVal.parentRelations.get(parent);
relations.add(relation);
} else {
List<Relation> relations = new LinkedList<Relation>();
relations.add(relation);
rVal.parentRelations.put(parent,relations);
}
}
//error check that all anchors are defined
Globals.web.getAnchors().errorCheck();
return rVal;
}
@ -117,6 +144,29 @@ public class KnowledgeWeb {
Relation.idIncrementer++;
}
rVal.relations = newRelations;
//make sure anchors object exists and any values are valid
if(rVal.anchors == null){
rVal.anchors = new AnchorNodes();
} else {
if(rVal.anchors.getSelf() != null){
int oldId = rVal.anchors.getSelf();
String lookupId = "node-" + rVal.getTag() + oldId;
int newNodeId = crossRelationLookupMap.get(lookupId);
rVal.anchors.setSelf(newNodeId);
}
if(rVal.anchors.getConcept() != null){
int oldId = rVal.anchors.getConcept();
String lookupId = "node-" + rVal.getTag() + oldId;
int newNodeId = crossRelationLookupMap.get(lookupId);
rVal.anchors.setConcept(newNodeId);
}
if(rVal.anchors.getQualia() != null){
int oldId = rVal.anchors.getQualia();
String lookupId = "node-" + rVal.getTag() + oldId;
int newNodeId = crossRelationLookupMap.get(lookupId);
rVal.anchors.setQualia(newNodeId);
}
}
//parse children
if(rVal.getChildren() != null && rVal.getChildren().size() > 0){
for(String child : rVal.getChildren()){
@ -142,6 +192,9 @@ public class KnowledgeWeb {
for(Relation relation : child.getRelations()){
parent.relations.put(relation.getId(),relation);
}
//hoist anchors from children
AnchorNodes.copyToParent(parent.anchors, child.anchors);
}
/**
@ -217,14 +270,6 @@ public class KnowledgeWeb {
}
return null;
}
/**
* Gets the node that represents the ai
* @return The node
*/
public Node getSelf(){
return this.getNode(this.selfId);
}
/**
* Gets the child webs of this web
@ -238,4 +283,25 @@ public class KnowledgeWeb {
return tag;
}
/**
* Gets the relations where the provided node is the parent of the relationship
* @param node The node
* @return The list of relations
*/
public List<Relation> getRelationsOfParentNode(Node node){
if(this.parentRelations.containsKey(node)){
return this.parentRelations.get(node);
} else {
return new LinkedList<Relation>();
}
}
/**
* Gets the anchor nodes for the web
* @return The anchor nodes
*/
public AnchorNodes getAnchors(){
return anchors;
}
}

View File

@ -29,6 +29,11 @@ public class Relation {
*/
int parent;
/**
* The web that contains the parent node
*/
String parentWeb;
/**
* The child of the relationship
*/
@ -77,6 +82,14 @@ public class Relation {
return this.childWeb;
}
/**
* Gets the tag for the web the parent belongs to (can be null or empty string if the web is the same as the parent)
* @return The tag for the web
*/
public String getParentWeb(){
return this.parentWeb;
}
}

View File

@ -0,0 +1,136 @@
package org.studiorailgun.knowledge.anchor;
import org.studiorailgun.Globals;
import org.studiorailgun.knowledge.Node;
/**
* Anchor nodes for the rest of the web
*/
public class AnchorNodes {
/**
* The node that represents the self
*/
Integer self;
/**
* The node that represents the idea of a concept
*/
Integer concept;
/**
* The concept of a qualia
*/
Integer qualia;
/**
* Gets the self
* @return The self node
*/
public Node getSelfNode() {
if(self == null){
return null;
}
return Globals.web.getNode(self);
}
/**
* Sets the self node ID
* @param id The self node's ID
*/
public void setSelf(int id) {
this.self = id;
}
/**
* Gets the id for the self node
* @return The id
*/
public Integer getSelf(){
return self;
}
/**
* Gets the idea of a concept
* @return The idea of a concept
*/
public Node getConceptNode() {
if(concept == null){
return null;
}
return Globals.web.getNode(concept);
}
/**
* Sets the concept node
* @param id The concept node
*/
public void setConcept(int id){
this.concept = id;
}
/**
* Gets the id for the concept node
* @return The id for the concept node
*/
public Integer getConcept(){
return concept;
}
/**
* Gets the idea of a qualia
* @return The idea of a qualia
*/
public Node getQualiaNode() {
if(qualia == null){
return null;
}
return Globals.web.getNode(qualia);
}
/**
* Sets the qualia node
* @param id The qualia node
*/
public void setQualia(int id){
this.qualia = id;
}
/**
* Gets the id for the qualia node
* @return The id for the qualia node
*/
public Integer getQualia(){
return qualia;
}
/**
* Copies anchor nodes from the child to the parent
* @param parentAnchors The parent anchors
* @param childAnchors The child anchors
*/
public static void copyToParent(AnchorNodes parentAnchors, AnchorNodes childAnchors){
if(childAnchors.getConcept() != null){
parentAnchors.setConcept(childAnchors.getConcept());
}
if(childAnchors.getSelf() != null){
parentAnchors.setSelf(childAnchors.getSelf());
}
if(childAnchors.getQualia() != null){
parentAnchors.setQualia(childAnchors.getQualia());
}
}
/**
* Checks for errors in the anchor definitions
*/
public void errorCheck(){
if(self == null){
throw new Error("Self undefined!");
}
if(concept == null){
throw new Error("Concept undefined!");
}
}
}

View File

@ -0,0 +1,41 @@
package org.studiorailgun.knowledge.query;
import java.util.List;
import java.util.stream.Collectors;
import org.studiorailgun.Globals;
import org.studiorailgun.knowledge.KnowledgeWeb;
import org.studiorailgun.knowledge.Node;
import org.studiorailgun.knowledge.Relation;
import org.studiorailgun.knowledge.query.filter.NameQueryFilter;
import org.studiorailgun.knowledge.types.RelationTypes;
/**
* Queries for dealing with instances of nodes
*/
public class InstanceQuery {
/**
* Gets the instances of a node
* @param web The web
* @param parent The parent node
* @return The list of instances of that node
*/
public static List<Node> getInstances(Node parent){
List<Relation> relations = Globals.web.getRelationsOfParentNode(parent);
return relations.stream().filter(relation -> relation.getName().equals(RelationTypes.INSTANCE_OF)).map(relation -> relation.getChild()).collect(Collectors.toList());
}
/**
* Gets a concept by its name
* @param web The web
* @param conceptName The name of the concept
* @return The concept
*/
public static Node getConcept(String conceptName){
Node concept = Globals.web.getAnchors().getConceptNode();
List<Node> concepts = InstanceQuery.getInstances(concept);
return NameQueryFilter.withNodeName(concepts, conceptName);
}
}

View File

@ -0,0 +1,24 @@
package org.studiorailgun.knowledge.query;
import java.util.List;
import java.util.stream.Collectors;
import org.studiorailgun.Globals;
import org.studiorailgun.knowledge.Node;
import org.studiorailgun.knowledge.types.RelationTypes;
/**
* Queries based around possession
*/
public class PossessionQuery {
/**
* Gets all the child nodes that this node possesses
* @param parent The parent node
* @return The child nodes that are possessed by this node
*/
public static List<Node> getPossessions(Node parent){
return Globals.web.getRelationsOfParentNode(parent).stream().filter(relation -> relation.getName().equals(RelationTypes.POSSESSION_OF)).map(relation -> relation.getChild()).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,50 @@
package org.studiorailgun.knowledge.query;
import java.util.List;
import java.util.stream.Collectors;
import org.studiorailgun.Globals;
import org.studiorailgun.conversation.evaluators.query.NounStack;
import org.studiorailgun.knowledge.Node;
import org.studiorailgun.knowledge.query.filter.NameQueryFilter;
import org.studiorailgun.knowledge.types.RelationTypes;
/**
* Performs a qualia query
*/
public class QualiaQuery {
/**
* Checks if this noun stack is querying for a qualia
* @param interrogativeStack The interrogative noun stack
* @return true if it is a qualia query, false otherwise
*/
public static boolean isQualiaClarificationQuery(NounStack interrogativeStack){
String originalWord = interrogativeStack.getIndexedWord().originalText();
List<Node> nodes = InstanceQuery.getInstances(Globals.web.getAnchors().getQualiaNode());
Node rVal = NameQueryFilter.withNodeName(nodes, originalWord);
return rVal != null;
}
/**
* Gets the type of qualia requested by the interrogative stack
* @param interrogativeStack The noun stack containing the qualia type
* @return The qualia type
*/
public static Node getRequestedQualiaType(NounStack interrogativeStack){
String originalWord = interrogativeStack.getIndexedWord().originalText();
List<Node> nodes = InstanceQuery.getInstances(Globals.web.getAnchors().getQualiaNode());
return NameQueryFilter.withNodeName(nodes, originalWord);
}
/**
* Gets all the qualia of an instance
* @param inst The instance
* @return The qualia
*/
public static List<Node> getQualiaOfInstance(Node inst){
return Globals.web.getRelationsOfParentNode(inst).stream().filter(relation -> relation.getName().equals(RelationTypes.QUALIA_OF)).map(relation -> relation.getChild()).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,24 @@
package org.studiorailgun.knowledge.query.filter;
import java.util.List;
import java.util.stream.Collectors;
import org.studiorailgun.knowledge.Node;
/**
* Node query filters based around names
*/
public class NameQueryFilter {
/**
* Filters a set of nodes to a singleton based on a name
*/
public static Node withNodeName(List<Node> nodes, String name){
List<Node> filtered = nodes.stream().filter(node -> !node.getName().equals(name)).collect(Collectors.toList());
if(filtered.size() != 1){
throw new UnsupportedOperationException("TODO: handle ambiguous case");
}
return filtered.get(0);
}
}

View File

@ -0,0 +1,28 @@
package org.studiorailgun.knowledge.query.filter;
import java.util.List;
import java.util.stream.Collectors;
import org.studiorailgun.knowledge.Node;
import org.studiorailgun.knowledge.query.PossessionQuery;
/**
* Query filters based on possession relationships
*/
public class PossessionQueryFilter {
/**
* Filters a set of nodes to a singleton based on a name
*/
public static Node withPossessor(List<Node> nodes, Node possessor){
List<Node> possessions = PossessionQuery.getPossessions(possessor);
List<Node> filtered = nodes.stream().filter(node -> {
return possessions.contains(node);
}).collect(Collectors.toList());
if(filtered.size() != 1){
throw new UnsupportedOperationException("TODO: handle ambiguous case");
}
return filtered.get(0);
}
}

View File

@ -0,0 +1,24 @@
package org.studiorailgun.knowledge.types;
/**
* All relation types
*/
public class RelationTypes {
/**
* An instance relationship
*/
public static final String INSTANCE_OF = "instanceOf";
/**
* A possession relationship
*/
public static final String POSSESSION_OF = "possessionOf";
/**
* A qualia of instance relationship
*/
public static final String QUALIA_OF = "qualiaOf";
}

View File

@ -12,8 +12,8 @@ public class ConvLoadingTests {
@Test
public void testLoadConversation(){
Globals.init("./data/webs/test/web.json");
assertEquals(Globals.web.getSelf(), Globals.conversation.getSelf().getData());
assertNotEquals(Globals.web.getSelf(), Globals.conversation.getOther().getData());
assertEquals(Globals.web.getAnchors().getSelfNode(), Globals.conversation.getSelf().getData());
assertNotEquals(Globals.web.getAnchors().getSelfNode(), Globals.conversation.getOther().getData());
}