From 7da5609da2907983c530344cf4e16bc5ca4de036 Mon Sep 17 00:00:00 2001 From: Rahul Arya Date: Tue, 9 Feb 2021 03:30:57 -0800 Subject: [PATCH 1/6] Add hinting app --- common/rpc/hinting.py | 56 ++++++++++++ hinting/deploy.yaml | 3 + hinting/main.py | 181 +++++++++++++++++++++++++++++++++++++++ hinting/requirements.txt | 3 + sicp/sicp/rpc.py | 1 + 5 files changed, 244 insertions(+) create mode 100644 common/rpc/hinting.py create mode 100644 hinting/deploy.yaml create mode 100644 hinting/main.py create mode 100644 hinting/requirements.txt diff --git a/common/rpc/hinting.py b/common/rpc/hinting.py new file mode 100644 index 00000000..55ee72ad --- /dev/null +++ b/common/rpc/hinting.py @@ -0,0 +1,56 @@ +from __future__ import annotations +from typing import Dict, List, Optional, TypedDict + +from common.rpc.utils import create_service + +service = create_service(__name__, "deploy.hosted") + + +class Messages(TypedDict): + file_contents: Dict[str, str] + grading: Dict[str, GradingInfo] + hinting: HintingInfo + ... + + +class RequiredGradingInfo(TypedDict): + passed: int + failed: int + locked: int + + +class GradingInfo(RequiredGradingInfo, total=False): + failed_outputs: List[str] + + +class HintingInfo(TypedDict): + flagged: bool + question: HintQuestionInfo + ... + + +class HintQuestionInfo(TypedDict): + pre_prompt: str + name: str + ... + + +class WWPDHintOutput(TypedDict): + hints: List[str] + + +class HintOutput(TypedDict): + message: str + post_prompt: Optional[str] + + +@service.route("/api/wwpd_hints") +def get_wwpd_hints(*, unlock_id: str, selected_options: List[str]) -> WWPDHintOutput: + ... + + +@service.route("/api/hints") +def get_hints( + *, assignment: str, test: str, messages: Messages, user: str +) -> HintOutput: + ... diff --git a/hinting/deploy.yaml b/hinting/deploy.yaml new file mode 100644 index 00000000..28b668e6 --- /dev/null +++ b/hinting/deploy.yaml @@ -0,0 +1,3 @@ +build_type: none +deploy_type: flask +concurrency: 40 diff --git a/hinting/main.py b/hinting/main.py new file mode 100644 index 00000000..a99c846f --- /dev/null +++ b/hinting/main.py @@ -0,0 +1,181 @@ +from random import choice +from typing import List + +from flask import Flask, redirect, request + +from common.db import connect_db +from common.html import html, make_row +from common.oauth_client import create_oauth_client, is_staff, login +from common.rpc.auth import read_spreadsheet +from common.rpc.hinting import Messages, get_hints, get_wwpd_hints +from common.rpc.utils import cached +from common.url_for import url_for + +with connect_db() as db: + db( + """CREATE TABLE IF NOT EXISTS sources ( + assignment varchar(512), + url varchar(512), + sheet varchar(512) +)""" + ) + +app = Flask(__name__, static_folder="", static_url_path="") +if __name__ == "__main__": + app.debug = True + +create_oauth_client(app, "61a-hinting") + + +@app.route("/") +def index(): + if not is_staff("cs61a"): + return login() + + with connect_db() as db: + sources = db( + "SELECT assignment, url, sheet FROM sources", + ).fetchall() + + insert_fields = f""" + + + + """ + + sources = "
".join( + make_row( + f'{assignment}: {url} {sheet}' + f'' + f'', + url_for("remove_source"), + ) + for assignment, url, sheet in sources + ) + + return html( + f""" +

Sources

+ {sources} +

Add Sources

+ {make_row(insert_fields, url_for("add_source"), "Add")} + """ + ) + + +@app.route("/add_source", methods=["POST"]) +def add_source(): + if not is_staff("cs61a"): + return login() + + assignment = request.form["assignment"] + url = request.form["url"] + sheet = request.form["sheet"] + + with connect_db() as db: + db( + "DELETE FROM sources WHERE assignment=%s", + [assignment], + ) + db( + "INSERT INTO sources VALUES (%s, %s, %s)", + [assignment, url, sheet], + ) + + return redirect(url_for("index")) + + +@app.route("/remove_source", methods=["POST"]) +def remove_source(): + if not is_staff("cs61a"): + return login() + + assignment = request.form["assignment"] + + with connect_db() as db: + db( + "DELETE FROM sources WHERE assignment=%s", + [assignment], + ) + + return redirect(url_for("index")) + + +def get_hint_source(assignment: str): + return ( + "https://docs.google.com/spreadsheets/d/1jjX1Zpak-pHKu-MuHKXd7y2ynWiTNkPfcStBDgX66l8/edit#gid=0", + "Sheet1", + ) + + +@cached() +def load_hint_source(*, assignment: str): + source = get_hint_source(assignment) + if source is None: + return [] + url, sheet_name = source + return read_spreadsheet(url=url, sheet_name=sheet_name) + + +def hint_lookup( + assignment: str, target_question: str, target_prompt: str, student_response: str +): + return [ + (hint, prompt) + for question, prompt, suite, case, needle, hint, followup in load_hint_source( + assignment=assignment + ) + if question in target_question + and prompt in target_prompt + and suite in student_response + and case in student_response + and needle in student_response + ] + + +@get_wwpd_hints.bind(app) +def get_wwpd_hints(*, unlock_id: str, selected_options: List[str]): + assignment, question, prompt = unlock_id.split("\n") + return dict( + hints=[ + hint + for hint, followup in hint_lookup( + assignment, question, prompt, "\n".join(selected_options) + ) + ] + ) + + +@get_hints.bind(app) +def get_hints(*, assignment: str, test: str, messages: Messages, user: str): + for question, results in messages.get("grading", {}).items(): + if question == messages["hinting"]["question"]["name"]: + failed_outputs = results.get("failed_outputs", []) + break + else: + failed_outputs = [] + + PS1 = ">>> " + PS2 = "... " + prompt = [] + rest = [] + for line in "\n".join(failed_outputs).split("\n"): + if line.startswith(PS1): + prompt = [line[len(PS1) :]] + rest = [] + elif line.startswith(PS2): + prompt.append(line[len(PS2) :]) + else: + rest.append(line) + + hints = hint_lookup(assignment, test, "\n".join(prompt), "\n".join(rest)) + + if hints: + message, post_prompt = choice(hints) + return dict(message=message, post_prompt=post_prompt) + else: + return {} + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/hinting/requirements.txt b/hinting/requirements.txt new file mode 100644 index 00000000..38bfafee --- /dev/null +++ b/hinting/requirements.txt @@ -0,0 +1,3 @@ +Flask==1.1.2 +gunicorn==20.0.4 +-r common/requirements.txt diff --git a/sicp/sicp/rpc.py b/sicp/sicp/rpc.py index bdd0d750..f6b08a08 100644 --- a/sicp/sicp/rpc.py +++ b/sicp/sicp/rpc.py @@ -19,3 +19,4 @@ from common.rpc import slack from common.rpc import ag_master from common.rpc import ag_worker +from common.rpc import hinting From 3d57eeace247dda3abb82e83f630c2df79c73837 Mon Sep 17 00:00:00 2001 From: Rahul Arya Date: Tue, 9 Feb 2021 03:31:05 -0800 Subject: [PATCH 2/6] add deps --- hinting/common | 1 + 1 file changed, 1 insertion(+) create mode 120000 hinting/common diff --git a/hinting/common b/hinting/common new file mode 120000 index 00000000..60d3b0a6 --- /dev/null +++ b/hinting/common @@ -0,0 +1 @@ +../common \ No newline at end of file From 9169f62330e82fe900e8796d656993bfd47f7a27 Mon Sep 17 00:00:00 2001 From: Rahul Arya Date: Thu, 11 Feb 2021 07:49:29 -0800 Subject: [PATCH 3/6] Update hinting app --- hinting/main.py | 78 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/hinting/main.py b/hinting/main.py index a99c846f..878fa186 100644 --- a/hinting/main.py +++ b/hinting/main.py @@ -1,7 +1,8 @@ from random import choice from typing import List -from flask import Flask, redirect, request +from flask import Flask, Response, redirect, request +from werkzeug.utils import escape from common.db import connect_db from common.html import html, make_row @@ -53,12 +54,65 @@ def index(): for assignment, url, sheet in sources ) + data = {} + for key in ["assignment", "question", "suite", "case", "prompt", "output"]: + if key in request.args: + data[key] = request.args[key] + else: + data = {} + break + + if data: + question = "Question " + data["question"] + output = data["output"] + f"\n Suite {data['suite']} Case {data['case']}" + hints = hint_lookup(data["assignment"], question, data["prompt"], output) + hint_rows = [ + f"

Hint: {escape(hint)}" + + (f" (Prompt: {escape(prompt)})" if prompt else "") + for hint, prompt in hints + ] + hint_html = f""" +

Hint Output

+ {"".join(hint_rows) if hints else "None"} + """ + else: + hint_html = "" + + def g(key): + return escape(data.get(key, "")) + + with connect_db() as db: + assignments = db("SELECT assignment FROM sources").fetchall() + return html( f"""

Sources

{sources}

Add Sources

{make_row(insert_fields, url_for("add_source"), "Add")} +

Test Hints

+
+

+ Assignment: + +

+ Question: +

+ Suite: +

+ Case: +

+ Prompt:
+ +

+ Student Output:
+ +

+ +

+ {hint_html} """ ) @@ -102,19 +156,25 @@ def remove_source(): def get_hint_source(assignment: str): - return ( - "https://docs.google.com/spreadsheets/d/1jjX1Zpak-pHKu-MuHKXd7y2ynWiTNkPfcStBDgX66l8/edit#gid=0", - "Sheet1", - ) + with connect_db() as db: + source = db( + "SELECT url, sheet FROM sources WHERE assignment=(%s)", [assignment] + ).fetchone() + + if source: + return source + return None -@cached() -def load_hint_source(*, assignment: str): +def load_hint_source(assignment: str, *, _cache={}, skip_cache=False): + if assignment in _cache and not skip_cache: + return _cache[assignment] source = get_hint_source(assignment) if source is None: return [] url, sheet_name = source - return read_spreadsheet(url=url, sheet_name=sheet_name) + _cache[assignment] = read_spreadsheet(url=url, sheet_name=sheet_name) + return _cache[assignment] def hint_lookup( @@ -123,7 +183,7 @@ def hint_lookup( return [ (hint, prompt) for question, prompt, suite, case, needle, hint, followup in load_hint_source( - assignment=assignment + assignment, skip_cache=True ) if question in target_question and prompt in target_prompt From 7bd16b39600d914d46d60b76f3841f2f0e7729ac Mon Sep 17 00:00:00 2001 From: Rahul Arya Date: Thu, 11 Feb 2021 08:58:17 -0800 Subject: [PATCH 4/6] cleanup imports --- hinting/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hinting/main.py b/hinting/main.py index 878fa186..e68713de 100644 --- a/hinting/main.py +++ b/hinting/main.py @@ -1,7 +1,7 @@ from random import choice from typing import List -from flask import Flask, Response, redirect, request +from flask import Flask, redirect, request from werkzeug.utils import escape from common.db import connect_db @@ -9,7 +9,6 @@ from common.oauth_client import create_oauth_client, is_staff, login from common.rpc.auth import read_spreadsheet from common.rpc.hinting import Messages, get_hints, get_wwpd_hints -from common.rpc.utils import cached from common.url_for import url_for with connect_db() as db: From d6df03e34182a5f4739e47db73bf4d77df5ece64 Mon Sep 17 00:00:00 2001 From: Rahul Arya Date: Thu, 11 Feb 2021 09:00:03 -0800 Subject: [PATCH 5/6] Add check_hints_available rpc method --- common/rpc/hinting.py | 5 +++++ hinting/main.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/common/rpc/hinting.py b/common/rpc/hinting.py index 55ee72ad..96aa0c64 100644 --- a/common/rpc/hinting.py +++ b/common/rpc/hinting.py @@ -54,3 +54,8 @@ def get_hints( *, assignment: str, test: str, messages: Messages, user: str ) -> HintOutput: ... + + +@service.route("/api/check_hints_available") +def check_hints_available(*, assignment: str) -> HintOutput: + ... diff --git a/hinting/main.py b/hinting/main.py index e68713de..cbc95df5 100644 --- a/hinting/main.py +++ b/hinting/main.py @@ -8,7 +8,12 @@ from common.html import html, make_row from common.oauth_client import create_oauth_client, is_staff, login from common.rpc.auth import read_spreadsheet -from common.rpc.hinting import Messages, get_hints, get_wwpd_hints +from common.rpc.hinting import ( + Messages, + check_hints_available, + get_hints, + get_wwpd_hints, +) from common.url_for import url_for with connect_db() as db: @@ -236,5 +241,14 @@ def get_hints(*, assignment: str, test: str, messages: Messages, user: str): return {} +@check_hints_available.bind(app) +def check_hints_available(*, assignment: str): + with connect_db() as db: + exists = db( + "SELECT COUNT(*) FROM sources WHERE assignment=(%s)", [assignment] + ).fetchone() + return bool(exists) + + if __name__ == "__main__": app.run(debug=True) From 39cb7505a4f8601cd1ffb0b7b80d228a7467e2a5 Mon Sep 17 00:00:00 2001 From: Rahul Arya Date: Thu, 11 Feb 2021 09:02:15 -0800 Subject: [PATCH 6/6] whoops --- common/rpc/hinting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/rpc/hinting.py b/common/rpc/hinting.py index 96aa0c64..9c82b8da 100644 --- a/common/rpc/hinting.py +++ b/common/rpc/hinting.py @@ -3,7 +3,7 @@ from common.rpc.utils import create_service -service = create_service(__name__, "deploy.hosted") +service = create_service(__name__) class Messages(TypedDict):