Skip to content

Commit 94ef07f

Browse files
added document timestamps and doc source option for communication pro… (#112)
- Added timestamp that keeps track of when documents are accessed - Added source function to communication protocol - Added filter to each dashboard to select docs accessed at a specific date and time
1 parent abffbf4 commit 94ef07f

15 files changed

Lines changed: 255 additions & 39 deletions

File tree

learning_observer/learning_observer/communication_protocol/executor.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ async def call_dispatch(functions, function_name, args, kwargs):
8484
>>> asyncio.run(call_dispatch({'double': double}, 'nonexistent', [1], {}))
8585
Traceback (most recent call last):
8686
...
87-
learning_observer.communication_protocol.exception.DAGExecutionException: ('Function nonexistent did not execute properly during call.', 'call_dispatch', {'function_name': 'nonexistent', 'args': [1], 'kwargs': {}, 'error': "'nonexistent'"})
87+
learning_observer.communication_protocol.exception.DAGExecutionException: ('Function nonexistent did not execute properly during call.', 'call_dispatch', {'function_name': 'nonexistent', 'args': [1], 'kwargs': {}, 'error': "'nonexistent'"}, ...)
8888
8989
9090
Raises an exception when the called function raises an exception.
9191
>>> asyncio.run(call_dispatch({'double': double}, 'double', [None], {}))
9292
Traceback (most recent call last):
9393
...
94-
learning_observer.communication_protocol.exception.DAGExecutionException: ('Function double did not execute properly during call.', 'call_dispatch', {'function_name': 'double', 'args': [None], 'kwargs': {}, 'error': 'Input cannot be None'})
94+
learning_observer.communication_protocol.exception.DAGExecutionException: ('Function double did not execute properly during call.', 'call_dispatch', {'function_name': 'double', 'args': [None], 'kwargs': {}, 'error': 'Input cannot be None'}, ...)
9595
"""
9696
try:
9797
function = functions[function_name]
@@ -182,7 +182,7 @@ def handle_join(left, right, left_on, right_on):
182182
... right=[{'rid': 2, 'right': True}, {'rid': 1, 'right': True}],
183183
... left_on='lid', right_on='rid'
184184
... )
185-
[{'error': 'KeyError: key not found', 'function': 'handle_join', 'error_provenance': {'target': {'left': True}, 'key': 'lid', 'exception': KeyError("Key lid not found in {'left': True}")}, 'timestamp': ... 'traceback': ... {'lid': 2, 'left': True, 'rid': 2, 'right': True}]
185+
[{'error': "KeyError: key `lid` not found in `dict_keys(['left'])`", 'function': 'handle_join', 'error_provenance': {'target': {'left': True}, 'key': 'lid', 'exception': KeyError("Key lid not found in {'left': True}")}, 'timestamp': ... 'traceback': ... {'lid': 2, 'left': True, 'rid': 2, 'right': True}]
186186
"""
187187
right_dict = {}
188188
for d in right:
@@ -196,7 +196,6 @@ def handle_join(left, right, left_on, right_on):
196196
for left_dict in left:
197197
try:
198198
lookup_key = get_nested_dict_value(left_dict, left_on)
199-
200199
right_dict_match = right_dict.get(lookup_key)
201200

202201
if right_dict_match:
@@ -206,11 +205,12 @@ def handle_join(left, right, left_on, right_on):
206205
merged_dict = left_dict
207206
result.append(merged_dict)
208207
except KeyError as e:
209-
result.append(DAGExecutionException(
210-
f'KeyError: key not found',
211-
inspect.currentframe().f_code.co_name,
212-
{'target': left_dict, 'key': left_on, 'exception': e}
213-
).to_dict())
208+
result.append(left_dict)
209+
# result.append(DAGExecutionException(
210+
# f'KeyError: key `{left_on}` not found in `{left_dict.keys()}`',
211+
# inspect.currentframe().f_code.co_name,
212+
# {'target': left_dict, 'key': left_on, 'exception': e}
213+
# ).to_dict())
214214

215215
return result
216216

@@ -345,13 +345,13 @@ async def handle_map(functions, function_name, values, value_path, func_kwargs=N
345345
and handled later by the DAG executor. In our text, we return both a normal result
346346
and the result of an exception being caught.
347347
>>> asyncio.run(handle_map({'double': double}, 'double', [{'path': i} for i in [1, 'fail']], 'path'))
348-
[{'output': 2, 'provenance': {'function': 'double', 'func_kwargs': {}, 'value': {'path': 1}, 'value_path': 'path'}}, {'error': 'Function double did not execute properly during map.', 'function': 'annotate_map_metadata', 'error_provenance': {'function': 'double', 'func_kwargs': {}, 'value': {'path': 'fail'}, 'value_path': 'path', 'error': 'Input must be an int'}, 'timestamp': ... 'traceback': '', 'provenance': {'function': 'double', 'func_kwargs': {}, 'value': {'path': 'fail'}, 'value_path': 'path'}}]
348+
[{'output': 2, 'provenance': {'function': 'double', 'func_kwargs': {}, 'value': {'path': 1}, 'value_path': 'path'}}, {'error': 'Function double did not execute properly during map.', 'function': 'annotate_map_metadata', 'error_provenance': {'function': 'double', 'func_kwargs': {}, 'value': {'path': 'fail'}, 'value_path': 'path', 'error': 'Input must be an int'}, 'timestamp': ... 'traceback': ... 'provenance': {'function': 'double', 'func_kwargs': {}, 'value': {'path': 'fail'}, 'value_path': 'path'}}]
349349
350350
Example of trying to call nonexistent function, `triple`
351351
>>> asyncio.run(handle_map({'double': double}, 'triple', [{'path': i} for i in range(2)], 'path'))
352352
Traceback (most recent call last):
353353
...
354-
learning_observer.communication_protocol.exception.DAGExecutionException: ('Could not find function `triple` in available functions.', 'handle_map', {'function_name': 'triple', 'available_functions': dict_keys(['double']), 'error': "'triple'"})
354+
learning_observer.communication_protocol.exception.DAGExecutionException: ('Could not find function `triple` in available functions.', 'handle_map', {'function_name': 'triple', 'available_functions': dict_keys(['double']), 'error': "'triple'"}, ...)
355355
"""
356356
if func_kwargs is None:
357357
func_kwargs = {}
@@ -424,7 +424,7 @@ async def handle_select(keys, fields):
424424
value = get_nested_dict_value(resulting_value, f)
425425
except KeyError as e:
426426
value = DAGExecutionException(
427-
f'KeyError: key not found',
427+
f'KeyError: key `{f}` not found in `{resulting_value.keys()}`',
428428
inspect.currentframe().f_code.co_name,
429429
{'target': resulting_value, 'key': f, 'exception': e}
430430
).to_dict()
@@ -481,7 +481,7 @@ def hack_handle_keys(function, STUDENTS=None, STUDENTS_path=None, RESOURCES=None
481481
fields = [
482482
{
483483
learning_observer.stream_analytics.fields.KeyField.STUDENT: get_nested_dict_value(s, STUDENTS_path), # TODO catch get_nested_dict_value errors
484-
learning_observer.stream_analytics.helpers.EventField('doc_id'): get_nested_dict_value(r, RESOURCES_path) # TODO catch get_nested_dict_value errors
484+
learning_observer.stream_analytics.helpers.EventField('doc_id'): get_nested_dict_value(r, RESOURCES_path, '') # TODO catch get_nested_dict_value errors
485485
} for s, r in zip(STUDENTS, RESOURCES)
486486
]
487487
provenances = [
@@ -670,8 +670,9 @@ async def visit(node_name):
670670
'error_path': error_path
671671
}
672672
error_texts = '\n'.join((f' {e}' for e in _find_error_messages(error)))
673+
tb = nodes[node_name]["error"].get("traceback", 'No traceback available')
673674
debug_log('ERROR:: Error occured within execution dag at '\
674-
f'{node_name}\n{nodes[node_name]["error"]["traceback"]}\n'\
675+
f'{node_name}\n{tb}\n'\
675676
f'{error_texts}')
676677
else:
677678
nodes[node_name] = await dispatch_node(nodes[node_name])

learning_observer/learning_observer/stream_analytics/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def make_key_from_json(js):
110110
]
111111

112112
if KeyField.STUDENT in js:
113-
user_id = j[sKeyField.STUDENT]
113+
user_id = js[KeyField.STUDENT]
114114

115115
aggregator_functions = sum(
116116
[

learning_observer/learning_observer/util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def get_nested_dict_value(d, key_str=None, default=MissingType.Missing):
151151
d = d
152152
else:
153153
if default == MissingType.Missing:
154-
raise KeyError(f'Key {key_str} not found in {d}')
154+
raise KeyError(f'Key `{key_str}` not found in {d}')
155155
return default
156156
return d
157157

@@ -165,11 +165,11 @@ def remove_nested_dict_value(d, key_str):
165165
if d is not None and key in d:
166166
d = d[key]
167167
else:
168-
raise KeyError(f'Key {key_str} not found in {d}')
168+
raise KeyError(f'Key `{key_str}` not found in {d}')
169169
if keys[-1] in d:
170170
return d.pop(keys[-1])
171171
else:
172-
raise KeyError(f'Key {key_str} not found in {d}')
172+
raise KeyError(f'Key `{key_str}` not found in {d}')
173173

174174

175175
def clean_json(json_object):

modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/assets/scripts.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ window.dash_clientside.bulk_essay_feedback = {
107107
/**
108108
* Sends data to server via websocket
109109
*/
110-
send_to_loconnection: async function (state, hash, clicks, query, systemPrompt, tags) {
110+
send_to_loconnection: async function (state, hash, clicks, docSrc, docDate, docTime, query, systemPrompt, tags) {
111111
if (state === undefined) {
112112
return window.dash_clientside.no_update
113113
}
@@ -116,14 +116,16 @@ window.dash_clientside.bulk_essay_feedback = {
116116
const decoded = decode_string_dict(hash.slice(1))
117117
if (!decoded.course_id) { return window.dash_clientside.no_update }
118118

119-
decoded.gpt_prompt = ''
120-
decoded.message_id = ''
119+
decoded.gpt_prompt = '';
120+
decoded.message_id = '';
121+
decoded.doc_source = docSrc;
122+
decoded.requested_timestamp = new Date(`${docDate}T${docTime}`).getTime().toString();
121123

122124
const trig = window.dash_clientside.callback_context.triggered[0]
123125
if (trig.prop_id.includes('bulk-essay-analysis-submit-btn')) {
124-
decoded.gpt_prompt = query
125-
decoded.system_prompt = systemPrompt
126-
decoded.tags = tags
126+
decoded.gpt_prompt = query;
127+
decoded.system_prompt = systemPrompt;
128+
decoded.tags = tags;
127129
}
128130

129131
const message = {

modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'''
55
import dash_bootstrap_components as dbc
66
from dash_renderjson import DashRenderjson
7+
import datetime
78
import lo_dash_react_components as lodrc
89

910
from dash import html, dcc, clientside_callback, ClientsideFunction, Output, Input, State, ALL
@@ -26,6 +27,10 @@
2627

2728
advanced_collapse = f'{prefix}-advanced-collapse'
2829
system_input = f'{prefix}-system-prompt-input'
30+
# document source
31+
doc_src = f'{prefix}-doc-src'
32+
doc_src_date = f'{prefix}-doc-src-date'
33+
doc_src_timestamp = f'{prefix}-doc-src-timestamp'
2934

3035
attachment_upload = f'{prefix}-attachment-upload'
3136
attachment_label = f'{prefix}-attachment-label'
@@ -64,7 +69,17 @@ def layout():
6469
dbc.InputGroupText('System prompt:'),
6570
dbc.Textarea(id=system_input, value=system_prompt)
6671
]),
67-
dcc.Store(id=attachment_store, data='')
72+
html.Div([
73+
dbc.Label('Document Source'),
74+
dbc.RadioItems(options=[
75+
{'label': 'Latest Document', 'value': 'latest' },
76+
{'label': 'Specific Time', 'value': 'ts'},
77+
], value='latest', id=doc_src),
78+
dbc.InputGroup([
79+
dcc.DatePickerSingle(id=doc_src_date, date=datetime.date.today()),
80+
dbc.Input(type='time', id=doc_src_timestamp, value=datetime.datetime.now().strftime("%H:%M"))
81+
])
82+
])
6883
], label='Advanced', id=advanced_collapse, is_open=False),
6984
])
7085

@@ -87,7 +102,8 @@ def layout():
87102
dbc.CardFooter([
88103
html.Small(id=attachment_warning_message, className='text-danger'),
89104
dbc.Button('Save', id=attachment_save, color='primary', n_clicks=0, class_name='float-end')
90-
])
105+
]),
106+
dcc.Store(id=attachment_store, data='')
91107
], class_name='h-100')
92108

93109
# query creator panel
@@ -136,16 +152,27 @@ def layout():
136152
return dcc.Loading(cont)
137153

138154

155+
# disbale document date/time options
156+
clientside_callback(
157+
ClientsideFunction(namespace='clientside', function_name='disable_doc_src_datetime'),
158+
Output(doc_src_date, 'disabled'),
159+
Output(doc_src_timestamp, 'disabled'),
160+
Input(doc_src, 'value')
161+
)
162+
139163
# send request on websocket
140164
clientside_callback(
141165
ClientsideFunction(namespace='bulk_essay_feedback', function_name='send_to_loconnection'),
142166
Output(websocket, 'send'),
143167
Input(websocket, 'state'), # used for initial setup
144168
Input('_pages_location', 'hash'),
145169
Input(submit, 'n_clicks'),
170+
Input(doc_src, 'value'),
171+
Input(doc_src_date, 'date'),
172+
Input(doc_src_timestamp, 'value'),
146173
State(query_input, 'value'),
147174
State(system_input, 'value'),
148-
State(tag_store, 'data')
175+
State(tag_store, 'data'),
149176
)
150177

151178
# enable/disabled submit based on query

modules/wo_common_student_errors/wo_common_student_errors/assets/scripts.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const categoryColors = {
2424
}
2525

2626
window.dash_clientside.common_student_errors = {
27-
send_to_loconnection: function (state, hash) {
27+
send_to_loconnection: function (state, hash, docSrc, docDate, docTime) {
2828
/**
2929
* When the hash of the URL changes, we send an updated query
3030
* to the Learning Observer server.
@@ -46,6 +46,8 @@ window.dash_clientside.common_student_errors = {
4646
} else {
4747
decoded.student_id = []
4848
}
49+
decoded.doc_source = docSrc;
50+
decoded.requested_timestamp = new Date(`${docDate}T${docTime}`).getTime().toString();
4951
const message = {
5052
wo: {
5153
execution_dag: 'writing_observer',

modules/wo_common_student_errors/wo_common_student_errors/dashboard/layout.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# package imports
77
import dash_bootstrap_components as dbc
88
from dash_renderjson import DashRenderjson
9+
import datetime
910
import lo_dash_react_components as lodrc
1011
import plotly.express as px
1112
import writing_observer.languagetool
@@ -27,6 +28,11 @@
2728
alert_text = f'{prefix}-alert-text'
2829
alert_error_dump = f'{prefix}-alert-error-dump'
2930

31+
# document source
32+
doc_src = f'{prefix}-doc-src'
33+
doc_src_date = f'{prefix}-doc-src-date'
34+
doc_src_timestamp = f'{prefix}-doc-src-timestamp'
35+
3036
# error per text length items
3137
error_per_length = f'{prefix}-errors-per-length-graph'
3238
error_per_length_tooltip = f'{prefix}-errors-per-length-tooltip'
@@ -49,6 +55,17 @@ def layout():
4955

5056
tooltip = dcc.Tooltip(id=error_per_length_tooltip, direction='bottom')
5157
overall_view = html.Div([
58+
html.Div([
59+
dbc.Label('Document Source'),
60+
dbc.RadioItems(options=[
61+
{'label': 'Latest Document', 'value': 'latest' },
62+
{'label': 'Specific Time', 'value': 'ts'},
63+
], value='latest', id=doc_src),
64+
dbc.InputGroup([
65+
dcc.DatePickerSingle(id=doc_src_date, date=datetime.date.today()),
66+
dbc.Input(type='time', id=doc_src_timestamp, value=datetime.datetime.now().strftime("%H:%M"))
67+
])
68+
]),
5269
activity.layout,
5370
dcc.Graph(
5471
id=error_per_length,
@@ -99,13 +116,25 @@ def layout():
99116
return dcc.Loading(cont)
100117

101118

119+
120+
# disbale document date/time options
121+
clientside_callback(
122+
ClientsideFunction(namespace='clientside', function_name='disable_doc_src_datetime'),
123+
Output(doc_src_date, 'disabled'),
124+
Output(doc_src_timestamp, 'disabled'),
125+
Input(doc_src, 'value')
126+
)
127+
102128
# send request to LOConnection
103129
clientside_callback(
104130
ClientsideFunction(namespace='common_student_errors', function_name='send_to_loconnection'),
105131
Output(websocket, 'send'),
106132
Output(individual.prefix, 'className'),
107133
Input(websocket, 'state'), # used for initial setup
108-
Input('_pages_location', 'hash')
134+
Input('_pages_location', 'hash'),
135+
Input(doc_src, 'value'),
136+
Input(doc_src_date, 'date'),
137+
Input(doc_src_timestamp, 'value'),
109138
)
110139

111140
# Update the url's hash based on errors per text length graph's selectedData

modules/wo_document_list/wo_document_list/assets/scripts.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ window.dash_clientside.document_list = {
107107
if (!decoded.course_id) { return window.dash_clientside.no_update }
108108

109109
decoded.assignment_id = assignment || ''
110-
decoded.tag_path = tag ? `tags.${tag}` : ''
110+
111+
decoded.tag_path = tag ? `tags.${tag}` : 'tags'
111112

112113
const message = {
113114
wo: {

modules/wo_highlight_dashboard/wo_highlight_dashboard/assets/scripts.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ window.dash_clientside.clientside = {
5959
return [text, true, data];
6060
},
6161

62+
disable_doc_src_datetime: function (value) {
63+
if (value === 'ts') {
64+
return [false, false];
65+
}
66+
return [true, true];
67+
},
68+
6269
change_sort_direction_icon: function(sort_check, sort_values) {
6370
// updates UI elements, does not handle sorting
6471
// based on the current sort, set the sort direction icon and sort text
@@ -401,7 +408,7 @@ window.dash_clientside.clientside = {
401408
return true;
402409
},
403410

404-
send_options_to_server: function(types, metrics, highlights, indicators, sort_by, course_id) {
411+
send_options_to_server: function(types, metrics, highlights, indicators, sort_by, course_id, doc_src, doc_date, doc_time) {
405412
// Send selected options to the server
406413
// TODO work on protocol for communicating with the
407414
//
@@ -410,15 +417,21 @@ window.dash_clientside.clientside = {
410417
// Input(settings.metric_checklist, 'value'),
411418
// Input(settings.highlight_checklist, 'value'),
412419
// Input(settings.indicator_checklist, 'value')
413-
// Input(settings.sort_by_checklist, 'value')
420+
// Input(settings.sort_by_checklist, 'value'),
421+
// Input(course_store, 'data'),
422+
// Input(settings.doc_src, 'value'),
423+
// Input(settings.doc_src_date, 'date'),
424+
// Input(settings.doc_src_timestamp, 'value')
414425
const options = metrics.concat(highlights).concat(indicators).concat(sort_by);
415426
const message = {
416427
docs_with_nlp: {
417428
execution_dag: 'writing_observer',
418429
target_exports: ['docs_with_nlp_annotations'],
419430
kwargs: {
420431
course_id: course_id,
421-
nlp_options: options
432+
nlp_options: options,
433+
doc_source: doc_src,
434+
requested_timestamp: new Date(`${doc_date}T${doc_time}`).getTime().toString()
422435
}
423436
}
424437
}

0 commit comments

Comments
 (0)