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

Commit 571ca46

Browse files
Retrievable Explanations (#75)
## What is the goal of this PR? As of typedb/typedb#5483, Grakn Core now: 1. has retrievable explanation trees 2. only the `ConceptMap` answer type has `Explanation` 3. `pattern` has moved from `Explanation` to `ConceptMap` 4. `pattern` contains IDs for each variable as well as the query pattern These changes are reflected in client python, as long with a `has_explanation()` method on `ConceptMap`. ## What are the changes implemented in this PR? * `ConceptMap` contains `query_pattern()` and `has_explanation()` methods * `Explanation` no longer contains `query_pattern()` but only a list of `ConceptMap` via `get_answers()` * Utilise new gRPC messages to retrieve layers of the explanation tree * Remove `Explanation` from any answers other than `ConceptMap` * Refactor and add tests for `Answer` types, and restructure Answers
1 parent 887c9f9 commit 571ca46

File tree

10 files changed

+325
-223
lines changed

10 files changed

+325
-223
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.idea
33
bazel-*
44
__pycache__
5+
**.pyc

BUILD

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,27 @@ py_test(
132132
python_version = "PY2"
133133
)
134134

135+
py_test(
136+
name = "test_answer",
137+
srcs = [
138+
"tests/integration/base.py",
139+
"tests/integration/test_answer.py"
140+
],
141+
deps = [
142+
":client_python",
143+
graknlabs_client_python_requirement("forbiddenfruit")
144+
],
145+
size = "large",
146+
data = ["@graknlabs_grakn_core//:assemble-mac-zip"],
147+
python_version = "PY2"
148+
)
149+
135150
test_suite(
136151
name = "test_integration",
137152
tests = [
138153
":test_concept",
139154
":test_grakn",
140-
":test_keyspace"
155+
":test_keyspace",
156+
":test_answer",
141157
]
142158
)

dependencies/graknlabs/dependencies.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def graknlabs_grakn_core():
2929
git_repository(
3030
name = "graknlabs_grakn_core",
3131
remote = "https://github.com/graknlabs/grakn",
32-
commit = "02a583bebe718ad77f65db2e61cacc44b9b82cb3" # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_grakn_core
32+
commit = "2c184db846a93ced9f8349bcd59c16bb626375b7" # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_grakn_core
3333
)
3434

3535
def graknlabs_protocol():

grakn/service/Session/TransactionService.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ def run_concept_method(self, concept_id, grpc_concept_method_req):
117117
response = self._communicator.send(tx_request)
118118
return response.conceptMethod_res.response
119119

120+
def explanation(self, explainable):
121+
""" Retrieve the explanation of a Concept Map from the server """
122+
tx_request = RequestBuilder.explanation(explainable)
123+
response = self._communicator.send(tx_request)
124+
return ResponseReader.ResponseReader.create_explanation(self, response.explanation_res)
120125

121126
def iterate(self, iterator_id):
122127
request = RequestBuilder.next_iter(iterator_id)

grakn/service/Session/util/RequestBuilder.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import grakn_protocol.session.Session_pb2 as transaction_messages
2222
import grakn_protocol.session.Concept_pb2 as concept_messages
23+
import grakn_protocol.session.Answer_pb2 as answer_messages
2324
from grakn.service.Session.util import enums
2425
from grakn.service.Session.Concept import BaseTypeMapping
2526

@@ -156,6 +157,27 @@ def put_rule(label, when, then):
156157

157158
# --- internal requests ---
158159

160+
@staticmethod
161+
def explanation(explainable):
162+
concept_map = {}
163+
for variable, concept in explainable.map().items():
164+
grpc_concept = RequestBuilder.ConceptMethod._concept_to_grpc_concept(concept)
165+
concept_map[variable] = grpc_concept
166+
167+
grpc_concept_map = answer_messages.ConceptMap(map=concept_map)
168+
169+
grpc_concept_map.hasExplanation = explainable.has_explanation()
170+
grpc_concept_map.pattern = explainable.query_pattern()
171+
172+
explanation_req = answer_messages.Explanation.Req()
173+
explanation_req.explainable.CopyFrom(grpc_concept_map)
174+
175+
transaction_req = transaction_messages.Transaction.Req()
176+
transaction_req.explanation_req.CopyFrom(explanation_req)
177+
178+
return transaction_req
179+
180+
159181
@staticmethod
160182
def next_iter(iterator_id):
161183
iterate_request = transaction_messages.Transaction.Iter.Req()

grakn/service/Session/util/ResponseReader.py

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

20-
import abc
2120
import datetime
2221
import six
2322
from grakn.service.Session.util import enums
@@ -117,13 +116,19 @@ def from_grpc_value_object(grpc_value_object):
117116
def iter_res_to_iterator(tx_service, iterator_id, next_iteration_handler):
118117
return ResponseIterator(tx_service, iterator_id, next_iteration_handler)
119118

119+
@staticmethod
120+
def create_explanation(tx_service, grpc_explanation_res):
121+
""" Convert gRPC explanation response to explanation object """
122+
grpc_list_of_concept_maps = grpc_explanation_res.explanation
123+
native_list_of_concept_maps = []
124+
for grpc_concept_map in grpc_list_of_concept_maps:
125+
native_list_of_concept_maps.append(AnswerConverter._create_concept_map(tx_service, grpc_concept_map))
126+
return Explanation(native_list_of_concept_maps)
127+
120128
class Explanation(object):
121-
def __init__(self, query_pattern, list_of_concept_maps):
122-
self._query_pattern = query_pattern
129+
130+
def __init__(self, list_of_concept_maps):
123131
self._concept_maps_list = list_of_concept_maps
124-
125-
def query_pattern(self):
126-
return self._query_pattern
127132

128133
def get_answers(self):
129134
""" Return answers this explanation is dependent on"""
@@ -133,24 +138,9 @@ def get_answers(self):
133138

134139
# ----- Different types of answers -----
135140

136-
class Answer(object):
137-
""" Top level answer, provides interface """
138-
139-
def __init__(self, explanation):
140-
self._explanation = explanation
141-
__init__.__annotations__ = {'explanation': Explanation}
142-
143-
@abc.abstractmethod
144-
def get(self):
145-
pass
141+
class AnswerGroup(object):
146142

147-
def explanation(self):
148-
return self._explanation
149-
150-
class AnswerGroup(Answer):
151-
152-
def __init__(self, owner_concept, answer_list, explanation):
153-
super(AnswerGroup, self).__init__(explanation)
143+
def __init__(self, owner_concept, answer_list):
154144
self._owner_concept = owner_concept
155145
self._answer_list = answer_list
156146

@@ -164,12 +154,13 @@ def answers(self):
164154
return self._answer_list
165155

166156

157+
class ConceptMap(object):
167158

168-
class ConceptMap(Answer):
169-
170-
def __init__(self, concept_map, explanations):
171-
super(ConceptMap, self).__init__(explanations)
172-
self._concept_map = concept_map
159+
def __init__(self, concept_map, query_pattern, has_explanation, tx_service):
160+
self._concept_map = concept_map
161+
self._has_explanation = has_explanation
162+
self._query_pattern = query_pattern
163+
self._tx_service = tx_service
173164

174165
def get(self, var=None):
175166
""" Get the indicated variable's Concept from the map or this ConceptMap """
@@ -180,9 +171,19 @@ def get(self, var=None):
180171
# TODO specialize exception
181172
raise GraknError("Variable {0} is not in the ConceptMap".format(var))
182173
return self._concept_map[var]
183-
""" Return ConceptMap """
184-
return self
185-
174+
175+
def query_pattern(self):
176+
return self._query_pattern
177+
178+
def has_explanation(self):
179+
return self._has_explanation
180+
181+
def explanation(self):
182+
if self._has_explanation:
183+
return self._tx_service.explanation(self)
184+
else:
185+
raise GraknError("Explanation not available on concept map: " + str(self))
186+
186187
def map(self):
187188
""" Get the map from Variable (str) to Concept objects """
188189
return self._concept_map
@@ -199,67 +200,54 @@ def is_empty(self):
199200
""" Check if the variable map is empty """
200201
return len(self._concept_map) == 0
201202

202-
class ConceptList(Answer):
203203

204-
def __init__(self, concept_id_list, explanation):
205-
super(ConceptList, self).__init__(explanation)
206-
self._concept_id_list = concept_id_list
207-
__init__.__annotations__ = {'explanation': Explanation}
204+
class ConceptList(object):
208205

209-
def get(self):
210-
""" Get this ConceptList """
211-
return self._concept_id_list
206+
def __init__(self, concept_id_list):
207+
self._concept_id_list = concept_id_list
212208

213209
def list(self):
214210
""" Get the list of concept IDs """
215211
return self._concept_id_list
216212

217-
class ConceptSet(Answer):
218213

219-
def __init__(self, concept_id_set, explanation):
220-
super(ConceptSet, self).__init__(explanation)
221-
self._concept_id_set = concept_id_set
222-
__init__.__annotations__ = {'explanation': Explanation}
214+
class ConceptSet(object):
223215

224-
def get(self):
225-
""" Get this ConceptSet """
226-
return self
216+
def __init__(self, concept_id_set):
217+
self._concept_id_set = concept_id_set
218+
__init__.__annotations__ = {'_concept_id_set': 'List[str]'}
227219

228220
def set(self):
229221
""" Return the set of Concept IDs within this ConceptSet """
230222
return self._concept_id_set
231223

224+
232225
class ConceptSetMeasure(ConceptSet):
233226

234-
def __init__(self, concept_id_set, number, explanation):
235-
super(ConceptSetMeasure, self).__init__(concept_id_set, explanation)
227+
def __init__(self, concept_id_set, number):
228+
super(ConceptSetMeasure, self).__init__(concept_id_set)
236229
self._measurement = number
237-
__init__.__annotations__ = {'explanation': Explanation}
230+
__init__.__annotations__ = {'_measurement': float}
238231

239232
def measurement(self):
240233
return self._measurement
241234

242235

243-
class Value(Answer):
236+
class Value(object):
244237

245-
def __init__(self, number, explanation):
246-
super(Value, self).__init__(explanation)
238+
def __init__(self, number):
247239
self._number = number
248-
__init__.__annotations__ = {'explanation': Explanation}
249-
250-
def get(self):
251-
""" Get this Value object """
252-
return self
240+
__init__.__annotations__ = {'number': float}
253241

254242
def number(self):
255243
""" Get as number (float or int) """
256244
return self._number
257245

258-
class Void(Answer):
246+
247+
class Void(object):
259248
def __init__(self, message):
260-
super(Void, self).__init__(None)
261249
self._message = message
262-
__init__.__annotations__ = {'explanation': Explanation, 'message': str}
250+
__init__.__annotations__ = {'message': str}
263251

264252
def message(self):
265253
""" Get the message on this Void answer type """
@@ -274,7 +262,7 @@ def convert(tx_service, grpc_answer):
274262
which_one = grpc_answer.WhichOneof('answer')
275263

276264
if which_one == 'conceptMap':
277-
return AnswerConverter._create_concept_map(tx_service, grpc_answer.conceptMap)
265+
return AnswerConverter._create_concept_map(tx_service, grpc_answer.conceptMap)
278266
elif which_one == 'answerGroup':
279267
return AnswerConverter._create_answer_group(tx_service, grpc_answer.answerGroup)
280268
elif which_one == 'conceptList':
@@ -298,56 +286,39 @@ def _create_concept_map(tx_service, grpc_concept_map_msg):
298286
for (variable, grpc_concept) in var_concept_map.items():
299287
answer_map[variable] = ConceptFactory.create_concept(tx_service, grpc_concept)
300288

301-
# build explanation
302-
explanation = AnswerConverter._create_explanation(tx_service, grpc_concept_map_msg.explanation)
303-
return ConceptMap(answer_map, explanation)
289+
query_pattern = grpc_concept_map_msg.pattern
290+
has_explanation = grpc_concept_map_msg.hasExplanation
291+
292+
return ConceptMap(answer_map, query_pattern, has_explanation, tx_service)
304293

305294
@staticmethod
306295
def _create_answer_group(tx_service, grpc_answer_group):
307296
grpc_owner_concept = grpc_answer_group.owner
308297
owner_concept = ConceptFactory.create_concept(tx_service, grpc_owner_concept)
309298
grpc_answers = list(grpc_answer_group.answers)
310299
answer_list = [AnswerConverter.convert(tx_service, grpc_answer) for grpc_answer in grpc_answers]
311-
explanation = AnswerConverter._create_explanation(tx_service, grpc_answer_group.explanation)
312-
return AnswerGroup(owner_concept, answer_list, explanation)
300+
return AnswerGroup(owner_concept, answer_list)
313301

314302
@staticmethod
315303
def _create_concept_list(tx_service, grpc_concept_list_msg):
316304
ids_list = list(grpc_concept_list_msg.list.ids)
317-
# build explanation
318-
explanation = AnswerConverter._create_explanation(tx_service, grpc_concept_list_msg.explanation)
319-
return ConceptList(ids_list, explanation)
305+
return ConceptList(ids_list)
320306

321307
@staticmethod
322308
def _create_concept_set(tx_service, grpc_concept_set_msg):
323309
ids_set = set(grpc_concept_set_msg.set.ids)
324-
# build explanation
325-
explanation = AnswerConverter._create_explanation(tx_service, grpc_concept_set_msg.explanation)
326-
return ConceptSet(ids_set, explanation)
310+
return ConceptSet(ids_set)
327311

328312
@staticmethod
329313
def _create_concept_set_measure(tx_service, grpc_concept_set_measure):
330314
concept_ids = list(grpc_concept_set_measure.set.ids)
331315
number = grpc_concept_set_measure.measurement.value
332-
explanation = AnswerConverter._create_explanation(tx_service, grpc_concept_set_measure.explanation)
333-
return ConceptSetMeasure(concept_ids, AnswerConverter._number_string_to_native(number), explanation)
316+
return ConceptSetMeasure(concept_ids, AnswerConverter._number_string_to_native(number))
334317

335318
@staticmethod
336319
def _create_value(tx_service, grpc_value_msg):
337320
number = grpc_value_msg.number.value
338-
# build explanation
339-
explanation = AnswerConverter._create_explanation(tx_service, grpc_value_msg.explanation)
340-
return Value(AnswerConverter._number_string_to_native(number), explanation)
341-
342-
@staticmethod
343-
def _create_explanation(tx_service, grpc_explanation):
344-
""" Convert grpc Explanation message into object """
345-
query_pattern = grpc_explanation.pattern
346-
grpc_list_of_concept_maps = grpc_explanation.answers
347-
native_list_of_concept_maps = []
348-
for grpc_concept_map in grpc_list_of_concept_maps:
349-
native_list_of_concept_maps.append(AnswerConverter._create_concept_map(tx_service, grpc_concept_map))
350-
return Explanation(query_pattern, native_list_of_concept_maps)
321+
return Value(AnswerConverter._number_string_to_native(number))
351322

352323
@staticmethod
353324
def _create_void(tx_service, grpc_void):
@@ -362,7 +333,6 @@ def _number_string_to_native(number):
362333
return float(number)
363334

364335

365-
366336
class ResponseIterator(six.Iterator):
367337
""" Retrieves next value in the Grakn response iterator """
368338

@@ -389,7 +359,7 @@ def collect_concepts(self):
389359
""" Helper method to retrieve concepts from a query() method """
390360
concepts = []
391361
for answer in self:
392-
if type(answer) != ConceptMap:
362+
if not isinstance(answer, ConceptMap):
393363
raise GraknError("Only use .collect_concepts on ConceptMaps returned by query()")
394364
concepts.extend(answer.map().values()) # get concept map => concepts
395365
return concepts

tests/integration/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# specific language governing permissions and limitations
1717
# under the License.
1818
#
19+
from __future__ import print_function
1920

2021
from unittest import TestCase
2122
from datetime import datetime
@@ -28,7 +29,6 @@
2829
import tempfile
2930
import zipfile
3031

31-
3232
class DummyContextManager(object):
3333
def __init__(self, *args, **kwargs):
3434
pass
@@ -93,8 +93,8 @@ def _datetime_to_timestamp(self):
9393
return int(diff.total_seconds())
9494

9595
if six.PY2:
96-
print 'Patching datetime.timestamp for PY2'
97-
print 'Patching unittest.TestCase.subTest for PY2'
96+
print('Patching datetime.timestamp for PY2')
97+
print('Patching unittest.TestCase.subTest for PY2')
9898
curse(datetime, 'timestamp', _datetime_to_timestamp)
9999
curse(TestCase, 'subTest', DummyContextManager)
100100

0 commit comments

Comments
 (0)