Skip to content

Commit

Permalink
One approach to having a validator plan for AI questions, still sever…
Browse files Browse the repository at this point in the history
…al to do items.
  • Loading branch information
thsparks committed Mar 28, 2024
1 parent a344259 commit 2372ed4
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 11 deletions.
20 changes: 16 additions & 4 deletions common-docs/teachertool/test/catalog-shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,27 @@
},
{
"id": "499F3572-E655-4DEE-953B-5F26BF0191D7",
"use": "block_used_n_times",
"template": "Long String: ${question}",
"description": "This is just a test for long string inputs.",
"use": "ai_question",
"template": "Ask Copilot: ${question}",
"description": "Experimental: AI outputs are inherently nondeterministic and may not be accurate. Use with caution and always review responses.",
"docPath": "/teachertool",
"params": [
{
"name": "question",
"type": "longString",
"paths": ["checks[0].blockCounts[0].blockId"]
"paths": ["checks[0].question"]
},
{
"name": "shareid",
"type": "system",
"default": "SHAREID",
"paths": ["checks[0].shareId"]
},
{
"name": "target",
"type": "system",
"default": "TARGET",
"paths": ["checks[0].target"]
}
]
},
Expand Down
13 changes: 13 additions & 0 deletions common-docs/teachertool/test/validator-plans-shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@
"count": 0
}
]
},
{
".desc": "Ask Copilot a question",
"name": "ai_question",
"threshold": -1,
"checks": [
{
"validator": "aiQuestion",
"question": "",
"shareId": "",
"target": ""
}
]
}
]
}
10 changes: 9 additions & 1 deletion localtypings/validatorPlan.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ declare namespace pxt.blocks {
}

export interface EvaluationResult {
result: boolean;
result?: boolean;
notes?: string;
}

export interface BlockFieldValueExistsCheck extends ValidatorCheckBase {
Expand All @@ -50,4 +51,11 @@ declare namespace pxt.blocks {
fieldValue: string;
blockType: string;
}

export interface AiQuestionValidatorCheck extends ValidatorCheckBase {
validator: "aiQuestion";
question: string;
shareId: string;
target: string; // TODO thsparks : Just look this up from the share id within deepprompt itself? Or in our backend?
}
}
51 changes: 48 additions & 3 deletions pxteditor/code-validation/runValidatorPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ import { validateBlockCommentsExist } from "./validateCommentsExist";
import { validateSpecificBlockCommentsExist } from "./validateSpecificBlockCommentsExist";
import { getNestedChildBlocks } from "./getNestedChildBlocks";

export function runValidatorPlan(usedBlocks: Blockly.Block[], plan: pxt.blocks.ValidatorPlan, planLib: pxt.blocks.ValidatorPlan[]): boolean {
export async function runValidatorPlan(usedBlocks: Blockly.Block[], plan: pxt.blocks.ValidatorPlan, planLib: pxt.blocks.ValidatorPlan[]): Promise<pxt.blocks.EvaluationResult> {
const startTime = Date.now();
let checksSucceeded = 0;
let successfulBlocks: Blockly.Block[] = [];
let notes: string = undefined;

function addToNote(note: string) {
if (!notes) {
notes = note;
} else {
notes += `\n${note}`;
}
}

for (const check of plan.checks) {
let checkPassed = false;
Expand All @@ -32,6 +41,10 @@ export function runValidatorPlan(usedBlocks: Blockly.Block[], plan: pxt.blocks.V
case "blockFieldValueExists":
[successfulBlocks, checkPassed] = [...runBlockFieldValueExistsValidation(usedBlocks, check as pxt.blocks.BlockFieldValueExistsCheck)];
break;
case "aiQuestion":
const response = await runAiQuestionValidation(check as pxt.blocks.AiQuestionValidatorCheck);
addToNote(response);
break;
default:
pxt.debug(`Unrecognized validator: ${check.validator}`);
checkPassed = false;
Expand All @@ -53,15 +66,16 @@ export function runValidatorPlan(usedBlocks: Blockly.Block[], plan: pxt.blocks.V
checksSucceeded += checkPassed ? 1 : 0;
}

const passed = checksSucceeded >= plan.threshold;
// If threshold is -1 then pass/fail does not apply.
const passed = plan.threshold < 0 ? undefined : checksSucceeded >= plan.threshold;

pxt.tickEvent("validation.evaluation_complete", {
plan: plan.name,
durationMs: Date.now() - startTime,
passed: `${passed}`,
});

return passed;
return { result: passed, notes };
}

function runBlocksExistValidation(usedBlocks: Blockly.Block[], inputs: pxt.blocks.BlocksExistValidatorCheck): [Blockly.Block[], boolean] {
Expand Down Expand Up @@ -104,3 +118,34 @@ function runBlockFieldValueExistsValidation(usedBlocks: Blockly.Block[], inputs:
});
return [blockResults.successfulBlocks, blockResults.passed];
}


// TODO thsparks - do we have a shared backend requests location in pxteditor? If not, should we make one?
export async function askCopilotQuestion(shareId: string, target: string, question: string): Promise<string> {
// TODO thsparks - any kind of retry logic, error handling?
// TODO thsparks - use pxt.Cloud.apiRoot instead of my staging endpoint.
const url = `https://makecode-app-backend-ppe-thsparks.azurewebsites.net/api/copilot/question`;
const data = { id: shareId, target, question }
const request = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const response = await request.text();

if (!response) {
throw new Error("Unable to reach copilot service.");
} else {
return response;
}
}


async function runAiQuestionValidation(inputs: pxt.blocks.AiQuestionValidatorCheck): Promise<string> {
// TODO thsparks - remove debug logs.
console.log(`Asking question: '${inputs.question}' on '${inputs.target}' project with shareId: '${inputs.shareId}'`);
const response = await askCopilotQuestion(inputs.shareId, inputs.target, inputs.question);
console.log(`Response: ${response}`);

return response;
}
2 changes: 1 addition & 1 deletion pxteditor/editorcontroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function bindEditorMessages(getEditorAsync: () => Promise<IProjectView>)
const blocks = projectView.getBlocks();
return runValidatorPlan(blocks, plan, planLib)})
.then (results => {
resp = { result: results };
resp = results;
});
}
case "gettoolboxcategories": {
Expand Down
2 changes: 2 additions & 0 deletions teachertool/src/services/autorunService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { runEvaluateAsync } from "../transforms/runEvaluateAsync";
let autorunTimer: NodeJS.Timeout | null = null;

export function poke() {
// TODO thsparks - somehow don't autorun for ai? It's expensive...

if (autorunTimer) {
clearTimeout(autorunTimer);
}
Expand Down
13 changes: 12 additions & 1 deletion teachertool/src/transforms/runEvaluateAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { showToast } from "./showToast";
import { setActiveTab } from "./setActiveTab";
import { setEvalResultOutcome } from "./setEvalResultOutcome";
import jp from "jsonpath";
import { getSystemParameter } from "../utils/getSystemParameter";
import { setEvalResult } from "./setEvalResult";
import { setEvalResultNotes } from "./setEvalResultNotes";

function generateValidatorPlan(
criteriaInstance: CriteriaInstance,
Expand Down Expand Up @@ -47,6 +50,10 @@ function generateValidatorPlan(
return undefined;
}

if (catalogParam.type === "system" && catalogParam.default) { // TODO thsparks - "keyword" instead of default?
param.value = getSystemParameter(catalogParam.default, teacherTool);
}

if (!param.value) {
// User didn't set a value for the parameter.
if (showErrors) {
Expand Down Expand Up @@ -97,8 +104,12 @@ export async function runEvaluateAsync(fromUserInteraction: boolean) {
const planResult = await runValidatorPlanAsync(plan, loadedValidatorPlans);

if (planResult) {
const result = planResult.result ? EvaluationStatus.Pass : EvaluationStatus.Fail;
const result = planResult.result === undefined ? EvaluationStatus.CompleteWithNoResult : planResult.result ? EvaluationStatus.Pass : EvaluationStatus.Fail;

setEvalResultOutcome(criteriaInstance.instanceId, result);
if (planResult.notes) {
setEvalResultNotes(criteriaInstance.instanceId, planResult.notes);
}
return resolve(true); // evaluation completed successfully, so return true (regardless of pass/fail)
} else {
dispatch(Actions.clearEvalResult(criteriaInstance.instanceId));
Expand Down
8 changes: 8 additions & 0 deletions teachertool/src/transforms/setEvalResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { stateAndDispatch } from "../state";
import * as Actions from "../state/actions";
import { CriteriaResult } from "../types/criteria";

export function setEvalResult(criteriaId: string, result: CriteriaResult) {
const { dispatch } = stateAndDispatch();
dispatch(Actions.setEvalResult(criteriaId, result));
}
2 changes: 1 addition & 1 deletion teachertool/src/types/criteria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface CriteriaInstance {
}

// Represents a parameter definition in a catalog criteria.
export type CriteriaParameterType = "string" | "longString" | "number" | "block";
export type CriteriaParameterType = "string" | "longString" | "number" | "block" | "system";
export interface CriteriaParameter {
name: string;
type: CriteriaParameterType;
Expand Down
1 change: 1 addition & 0 deletions teachertool/src/types/errorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export enum ErrorCode {
fetchJsonDocAsync = "fetchJsonDocAsync",
missingParameter = "missingParameter",
selectedBlockWithoutOptions = "selectedBlockWithoutOptions",
unrecognizedSystemParameter = "unrecognizedSystemParameter",
}
15 changes: 15 additions & 0 deletions teachertool/src/utils/getSystemParameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { logError } from "../services/loggingService";
import { AppState } from "../state/state";
import { ErrorCode } from "../types/errorCode";

export function getSystemParameter(keyword: string, state: AppState): string | undefined {
switch (keyword) {
case "SHAREID":
return state.projectMetadata?.id;
case "TARGET":
return state.projectMetadata?.target;
default:
logError(ErrorCode.unrecognizedSystemParameter, "Unrecognized system parameter", { keyword });
return undefined;
}
};

0 comments on commit 2372ed4

Please sign in to comment.