Skip to content
This repository was archived by the owner on Oct 9, 2023. It is now read-only.

Commit eae5454

Browse files
author
Alex Walker
authored
Implement explanations (#213)
## What is the goal of this PR? Following typedb/typedb#6271 and the corresponding protocol change in typedb/typedb-protocol#131 we implement Explanations, Explainable concept maps, and the explain() query API, which allows users to stream Explanations on demand **note: explain query or transaction option must be set to `true`** ## What are the changes implemented in this PR? * Implement `Explanation` objects, and extend `ConceptMap` to contain `Explainables` * Add the `QueryManager.explain(Explainable)` API to retrieve all direct explanations (1-rule layer)
1 parent f3111a3 commit eae5454

File tree

18 files changed

+403
-95
lines changed

18 files changed

+403
-95
lines changed

dependencies/graknlabs/artifacts.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def graknlabs_grakn_core_artifacts():
2727
artifact_name = "grakn-core-server-{platform}-{version}.{ext}",
2828
tag_source = deployment["artifact.release"],
2929
commit_source = deployment["artifact.snapshot"],
30-
commit = "136d9e134a59ab4207d9d45de241997918e0f798",
30+
commit = "a36868f1e8a34188eb371dee329ed499399cb40f",
3131
)
3232

3333
def graknlabs_grakn_cluster_artifacts():
@@ -37,5 +37,5 @@ def graknlabs_grakn_cluster_artifacts():
3737
artifact_name = "grakn-cluster-all-{platform}-{version}.{ext}",
3838
tag_source = deployment_private["artifact.release"],
3939
commit_source = deployment_private["artifact.snapshot"],
40-
commit = "c361e8e4b3f1e14aa8fde4c284ebf2cb2113b99f"
40+
commit = "f08e4d9e194ee7e1806995377c8b0a7bd56903ec"
4141
)

dependencies/graknlabs/repositories.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def graknlabs_common():
3030
git_repository(
3131
name = "graknlabs_common",
3232
remote = "https://github.com/graknlabs/common",
33-
tag = "2.0.0-alpha-9" # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_common
33+
tag = "2.0.0" # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_common
3434
)
3535

3636
def graknlabs_behaviour():

grakn/api/answer/concept_map.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# under the License.
1818
#
1919
from abc import ABC, abstractmethod
20-
from typing import Mapping, Iterable
20+
from typing import Mapping, Iterable, Tuple
2121

2222
from grakn.api.concept.concept import Concept
2323

@@ -35,3 +35,43 @@ def concepts(self) -> Iterable[Concept]:
3535
@abstractmethod
3636
def get(self, variable: str) -> Concept:
3737
pass
38+
39+
@abstractmethod
40+
def explainables(self) -> "ConceptMap.Explainables":
41+
pass
42+
43+
class Explainables(ABC):
44+
45+
@abstractmethod
46+
def relation(self, variable: str) -> "ConceptMap.Explainable":
47+
pass
48+
49+
@abstractmethod
50+
def attribute(self, variable: str) -> "ConceptMap.Explainable":
51+
pass
52+
53+
@abstractmethod
54+
def ownership(self, owner: str, attribute: str) -> "ConceptMap.Explainable":
55+
pass
56+
57+
@abstractmethod
58+
def relations(self) -> Mapping[str, "ConceptMap.Explainable"]:
59+
pass
60+
61+
@abstractmethod
62+
def attributes(self) -> Mapping[str, "ConceptMap.Explainable"]:
63+
pass
64+
65+
@abstractmethod
66+
def ownerships(self) -> Mapping[Tuple[str, str], "ConceptMap.Explainable"]:
67+
pass
68+
69+
class Explainable(ABC):
70+
71+
@abstractmethod
72+
def conjunction(self) -> str:
73+
pass
74+
75+
@abstractmethod
76+
def explainable_id(self) -> int:
77+
pass

grakn/api/logic/explanation.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
from abc import ABC, abstractmethod
20+
from typing import Mapping, Set
21+
22+
from grakn.api.answer.concept_map import ConceptMap
23+
from grakn.api.logic.rule import Rule
24+
25+
26+
class Explanation(ABC):
27+
28+
@abstractmethod
29+
def rule(self) -> Rule:
30+
pass
31+
32+
@abstractmethod
33+
def conclusion(self) -> ConceptMap:
34+
pass
35+
36+
@abstractmethod
37+
def condition(self) -> ConceptMap:
38+
pass
39+
40+
@abstractmethod
41+
def variable_mapping(self) -> Mapping[str, Set[str]]:
42+
pass

grakn/api/query/query_manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from grakn.api.answer.concept_map_group import ConceptMapGroup
2424
from grakn.api.answer.numeric import Numeric
2525
from grakn.api.answer.numeric_group import NumericGroup
26+
from grakn.api.logic.explanation import Explanation
2627
from grakn.api.options import GraknOptions
2728
from grakn.api.query.future import QueryFuture
2829

@@ -57,6 +58,10 @@ def delete(self, query: str, options: GraknOptions = None) -> QueryFuture:
5758
def update(self, query: str, options: GraknOptions = None) -> Iterator[ConceptMap]:
5859
pass
5960

61+
@abstractmethod
62+
def explain(self, explainable: ConceptMap.Explainable, options: GraknOptions = None) -> Iterator[Explanation]:
63+
pass
64+
6065
@abstractmethod
6166
def define(self, query: str, options: GraknOptions = None) -> QueryFuture:
6267
pass

grakn/common/exception.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ def __init__(self, code: int, message: str):
102102
BAD_ENCODING = ConceptErrorMessage(5, "The encoding '%s' was not recognised.")
103103
BAD_VALUE_TYPE = ConceptErrorMessage(6, "The value type '%s' was not recognised.")
104104
BAD_ATTRIBUTE_VALUE = ConceptErrorMessage(7, "The attribute value '%s' was not recognised.")
105-
GET_HAS_WITH_MULTIPLE_FILTERS = ConceptErrorMessage(8, "Only one filter can be applied at a time to get_has. The possible filters are: [attribute_type, attribute_types, only_key]")
105+
NONEXISTENT_EXPLAINABLE_CONCEPT = ConceptErrorMessage(8, "The concept identified by '%s' is not explainable.")
106+
NONEXISTENT_EXPLAINABLE_OWNERSHIP = ConceptErrorMessage(9, "The ownership by owner '%s' of attribute '%s' is not explainable.")
107+
GET_HAS_WITH_MULTIPLE_FILTERS = ConceptErrorMessage(10, "Only one filter can be applied at a time to get_has. The possible filters are: [attribute_type, attribute_types, only_key]")
106108

107109

108110
class QueryErrorMessage(ErrorMessage):

grakn/common/rpc/request_builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ def query_manager_update_req(query: str, options: options_proto.Options):
229229
return query_manager_req(query_mgr_req, options)
230230

231231

232+
def query_manager_explain_req(explainable_id: int, options: options_proto.Options):
233+
query_mgr_req = query_proto.QueryManager.Req()
234+
explain_req = query_proto.QueryManager.Explain.Req()
235+
explain_req.explainable_id = explainable_id
236+
query_mgr_req.explain_req.CopyFrom(explain_req)
237+
return query_manager_req(query_mgr_req, options)
238+
239+
232240
# ConceptManager
233241

234242
def concept_manager_req(concept_mgr_req: concept_proto.ConceptManager.Req):

grakn/concept/answer/concept_map.py

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,29 @@
1717
# under the License.
1818
#
1919

20-
from typing import Mapping
20+
from typing import Mapping, Dict, Tuple
2121

2222
import grakn_protocol.common.answer_pb2 as answer_proto
2323

2424
from grakn.api.answer.concept_map import ConceptMap
2525
from grakn.api.concept.concept import Concept
26-
from grakn.common.exception import GraknClientException, VARIABLE_DOES_NOT_EXIST
26+
from grakn.common.exception import GraknClientException, VARIABLE_DOES_NOT_EXIST, NONEXISTENT_EXPLAINABLE_CONCEPT, \
27+
NONEXISTENT_EXPLAINABLE_OWNERSHIP
2728
from grakn.concept.proto import concept_proto_reader
2829

2930

3031
class _ConceptMap(ConceptMap):
3132

32-
def __init__(self, mapping: Mapping[str, Concept]):
33+
def __init__(self, mapping: Mapping[str, Concept], explainables: ConceptMap.Explainables = None):
3334
self._map = mapping
35+
self._explainables = explainables
3436

3537
@staticmethod
36-
def of(concept_map_proto: answer_proto.ConceptMap) -> "_ConceptMap":
38+
def of(res: answer_proto.ConceptMap) -> "_ConceptMap":
3739
variable_map = {}
38-
for res_var in concept_map_proto.map:
39-
variable_map[res_var] = concept_proto_reader.concept(concept_map_proto.map[res_var])
40-
return _ConceptMap(variable_map)
40+
for res_var in res.map:
41+
variable_map[res_var] = concept_proto_reader.concept(res.map[res_var])
42+
return _ConceptMap(variable_map, _ConceptMap.Explainables.of(res.explainables))
4143

4244
def map(self):
4345
return self._map
@@ -51,6 +53,9 @@ def get(self, variable: str):
5153
raise GraknClientException.of(VARIABLE_DOES_NOT_EXIST, variable)
5254
return concept
5355

56+
def explainables(self) -> ConceptMap.Explainables:
57+
return self._explainables
58+
5459
def __str__(self):
5560
return "".join(map(lambda var: "[" + var + "/" + str(self._map[var]) + "]", sorted(self._map.keys())))
5661

@@ -63,3 +68,87 @@ def __eq__(self, other):
6368

6469
def __hash__(self):
6570
return hash(self._map)
71+
72+
class Explainables(ConceptMap.Explainables):
73+
74+
def __init__(self, relations: Mapping[str, ConceptMap.Explainable] = None, attributes: Mapping[str, ConceptMap.Explainable] = None, ownerships: Mapping[Tuple[str, str], ConceptMap.Explainable] = None):
75+
self._relations = relations
76+
self._attributes = attributes
77+
self._ownerships = ownerships
78+
79+
@staticmethod
80+
def of(explainables: answer_proto.Explainables):
81+
relations: Dict[str, ConceptMap.Explainable] = {}
82+
for [var, explainable] in explainables.relations.items():
83+
relations[var] = _ConceptMap.Explainable.of(explainable)
84+
attributes: Dict[str, ConceptMap.Explainable] = {}
85+
for [var, explainable] in explainables.attributes.items():
86+
attributes[var] = _ConceptMap.Explainable.of(explainable)
87+
ownerships: Dict[Tuple[str, str], ConceptMap.Explainable] = {}
88+
for [var, owned_map] in explainables.ownerships.items():
89+
for [owned, explainable] in owned_map.owned.items():
90+
ownerships[(var, owned)] = _ConceptMap.Explainable.of(explainable)
91+
return _ConceptMap.Explainables(relations, attributes, ownerships)
92+
93+
def relation(self, variable: str) -> "ConceptMap.Explainable":
94+
explainable = self._relations.get(variable)
95+
if not explainable:
96+
raise GraknClientException.of(NONEXISTENT_EXPLAINABLE_CONCEPT, variable)
97+
return explainable
98+
99+
def attribute(self, variable: str) -> "ConceptMap.Explainable":
100+
explainable = self._attributes.get(variable)
101+
if not explainable:
102+
raise GraknClientException.of(NONEXISTENT_EXPLAINABLE_CONCEPT, variable)
103+
return explainable
104+
105+
def ownership(self, owner: str, attribute: str) -> "ConceptMap.Explainable":
106+
explainable = self._ownerships.get((owner, attribute))
107+
if not explainable:
108+
raise GraknClientException.of(NONEXISTENT_EXPLAINABLE_OWNERSHIP, (owner, attribute))
109+
return explainable
110+
111+
def relations(self) -> Mapping[str, "ConceptMap.Explainable"]:
112+
return self._relations
113+
114+
def attributes(self) -> Mapping[str, "ConceptMap.Explainable"]:
115+
return self._attributes
116+
117+
def ownerships(self) -> Mapping[Tuple[str, str], "ConceptMap.Explainable"]:
118+
return self._ownerships
119+
120+
def __eq__(self, other):
121+
if other is self:
122+
return True
123+
if not other or type(other) != type(self):
124+
return False
125+
return self._relations == other._relations and self._attributes == other._attributes and self._ownerships == other._ownerships
126+
127+
def __hash__(self):
128+
return hash((self._relations, self._attributes, self._ownerships))
129+
130+
class Explainable(ConceptMap.Explainable):
131+
132+
def __init__(self, conjunction: str, explainable_id: int):
133+
self._conjunction = conjunction
134+
self._explainable_id = explainable_id
135+
136+
@staticmethod
137+
def of(explainable: answer_proto.Explainable):
138+
return _ConceptMap.Explainable(explainable.conjunction, explainable.id)
139+
140+
def conjunction(self) -> str:
141+
return self._conjunction
142+
143+
def explainable_id(self) -> int:
144+
return self._explainable_id
145+
146+
def __eq__(self, other):
147+
if other is self:
148+
return True
149+
if not other or type(other) != type(self):
150+
return False
151+
return self._explainable_id
152+
153+
def __hash__(self):
154+
return hash(self._explainable_id)

grakn/concept/thing/thing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def get_iid(self):
4545
return self._iid
4646

4747
def __str__(self):
48-
return type(self).__name__ + "[iid:" + self.get_iid() + "]"
48+
return "%s[%s:%s]" % (type(self).__name__, self.get_type().get_label(), self.get_iid())
4949

5050
def __eq__(self, other):
5151
if other is self:

grakn/logic/explanation.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
from typing import Mapping, Set
20+
21+
import grakn_protocol.common.logic_pb2 as logic_proto
22+
23+
from grakn.api.answer.concept_map import ConceptMap
24+
from grakn.api.logic.explanation import Explanation
25+
from grakn.api.logic.rule import Rule
26+
from grakn.concept.answer.concept_map import _ConceptMap
27+
from grakn.logic.rule import _Rule
28+
29+
30+
def _var_mapping_of(var_mapping: Mapping[str, logic_proto.Explanation.VarList]):
31+
mapping = {}
32+
for from_ in var_mapping:
33+
tos = var_mapping[from_]
34+
mapping[from_] = set(tos.vars)
35+
return mapping
36+
37+
38+
class _Explanation(Explanation):
39+
40+
def __init__(self, rule: Rule, variable_mapping: Mapping[str, Set[str]], conclusion: ConceptMap, condition: ConceptMap):
41+
self._rule = rule
42+
self._variable_mapping = variable_mapping
43+
self._conclusion = conclusion
44+
self._condition = condition
45+
46+
@staticmethod
47+
def of(explanation: logic_proto.Explanation):
48+
return _Explanation(_Rule.of(explanation.rule), _var_mapping_of(explanation.var_mapping),
49+
_ConceptMap.of(explanation.conclusion), _ConceptMap.of(explanation.condition))
50+
51+
def rule(self) -> Rule:
52+
return self._rule
53+
54+
def variable_mapping(self) -> Mapping[str, Set[str]]:
55+
return self._variable_mapping
56+
57+
def conclusion(self) -> ConceptMap:
58+
return self._conclusion
59+
60+
def condition(self) -> ConceptMap:
61+
return self._condition
62+
63+
def __str__(self):
64+
return "Explanation[rule: %s, variable_mapping: %s, then_answer: %s, when_answer: %s]" % (self._rule, self._variable_mapping, self._conclusion, self._condition)
65+
66+
def __eq__(self, other):
67+
if other is self:
68+
return True
69+
if not other or type(self) != type(other):
70+
return False
71+
return self._rule == other._rule and self._variable_mapping == other._variable_mapping and self._conclusion == other._conclusion and self._condition == other._condition
72+
73+
def __hash__(self):
74+
return hash((self._rule, self._variable_mapping, self._conclusion, self._condition))

0 commit comments

Comments
 (0)