Skip to content

Commit

Permalink
Comment-creating function
Browse files Browse the repository at this point in the history
  • Loading branch information
bgoldowsky committed Oct 1, 2024
1 parent e157f38 commit 734bcd7
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 6 deletions.
80 changes: 80 additions & 0 deletions functions-v2/src/on-analysis-image-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {onDocumentWritten} from "firebase-functions/v2/firestore";
import * as logger from "firebase-functions/logger";
import * as admin from "firebase-admin";
import {getDatabase} from "firebase-admin/database";

// This is one of three functions for AI analysis of documents:
// 1. Watch for changes to the lastUpdatedAt metadata field and write a queue of docs to process
// 2. Create screenshots of those documents
// 3. (This function) Send those screenshots to the AI service for processing, and create comments with the results

// NOTE: these should match the user specified in src/models/stores/user-types.ts
const commenterName = "Ada Insight";
const commenterUid = "ada_insight_1";

// TODO once we have actual screenshots to work with, we can replace this sample data with call to AI service
const sampleMessage = {
refusal: null,
content: {
category: "function",
keyIndicators: ["Water Saving Idea", "rain barrel"],
discussion: "The document mentions a 'Water Saving Idea' and refers specifically to a 'rain barrel', " +
"indicating a focus on the functionality of the design in terms of saving water.",
},
};

// Make a comment in Firestore
export const onAnalysisImageReady =
onDocumentWritten("demo/AI/portals/demo/aiProcessingQueue/{docId}",
async (event) => {
const {docId} = event.params;
const queueDocRef = getDatabase().ref(event.document);

// Document should contain { metadataPath, updated, status }
const snap = await queueDocRef.once("value");
if (snap.val().status !== "imaged") return;

// TODO: do AI analysis here to construct the comment content.
if (sampleMessage.refusal) {
logger.info("AI refused to comment on", event.document, sampleMessage.refusal);
return;
}
const tags = [sampleMessage.content.category];
const message = sampleMessage.content.discussion +
` Key Indicators: ${sampleMessage.content.keyIndicators.join(", ")}`;

const commentsPath = `demo/AI/documents/${docId}/comments`;

// Look for existing comment
const firestore = admin.firestore();
const existing = await firestore.collection(commentsPath)
.where("uid", "==", commenterUid).get().then((snapshot) => {
console.log("did query, got", snapshot.size);
if (snapshot.size > 0) {
return snapshot.docs[0];
} else {
return undefined;
}
});

if (existing) {
logger.info("Updating existing comment for", event.document);
await existing.ref.update({
tags,
content: message,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
} else {
logger.info("Creating comment for", event.document);
await firestore.collection(commentsPath).add({
content: message,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
name: commenterName,
uid: commenterUid,
});
}

await queueDocRef.update({
status: "done",
});
});
6 changes: 3 additions & 3 deletions functions-v2/src/on-analyzable-doc-written.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import {getDatabase} from "firebase-admin/database";
import * as logger from "firebase-functions/logger";
// import * as admin from "firebase-admin";

// This is one of what will likely be multiple functions for AI analysis of documents:
// This is one of three functions for AI analysis of documents:
// 1. (This function) watch for changes to the lastUpdatedAt metadata field and write a queue of docs to process
// 2. Create screenshots of those documents
// 3. Send those screenshots to the AI service for processing, and create document comments with the results

// For now, restrict processing to a particular root for testing.
// TODO later this will be a parameter.
// TODO later we will open this up to all documents, and {root} will be a parameter.
const root = "demo/AI/portals/demo";

// Location of the queue of documents to process, relative to the root
Expand All @@ -32,7 +32,7 @@ export const onAnalyzableDocWritten =
[docId]: {
metadataPath: `classes/${classId}/users/${userId}/documentMetadata/${docId}`,
updated: timestamp,
status: "unanalyzed",
status: "updated",
},
});
});
Expand Down
7 changes: 4 additions & 3 deletions functions-v2/src/send-to-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import {onDocumentWritten} from "firebase-functions/v2/firestore";
import * as logger from "firebase-functions/logger";
import categorizeDocument from "../lib/src/ai-categorize-document";

// Ultimately this should take screenshots generated of user documents and pass them to the AI service for processing.
// We can't really do this until there's a screenshotting service implemented.
// TODO this function will go away and the call to the AI service will be incorporated
// into on-analysis-image-ready.ts.

// As a proof of concept, this function just sends a sample image from the filesystem.

// Load the image data from disk and base64 encode it
// const sampleImageFile = "./image0.png";
const sampleImageFile = "./2/1350683-problem--O1IJaBTDBU86PD-PKbS.png";

// TODO - not yet sure what should trigger this function. It needs to run after a screenshot is generated.
export const onProcessingQueueWritten =
onDocumentWritten("demo/AI/portals/demo/aiProcessingQueue/{docId}",
async (event) => {
Expand Down
165 changes: 165 additions & 0 deletions functions-v2/test/create-comment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import {
clearFirestoreData,
} from "firebase-functions-test/lib/providers/firestore";
import {Firestore} from "firebase-admin/firestore";
import * as logger from "firebase-functions/logger";
import {getDatabase} from "firebase-admin/database";
import * as admin from "firebase-admin";

import {initialize, projectConfig} from "./initialize";
import {onAnalysisImageReady} from "../src/on-analysis-image-ready";

jest.mock("firebase-functions/logger");

const {fft, cleanup} = initialize();


let firestore: Firestore;

describe("functions", () => {
beforeEach(async () => {
firestore = admin.firestore();
await clearFirestoreData(projectConfig);
await getDatabase().ref("demo").set(null);
});

describe("onProcessingQueueWritten", () => {
test("does nothing when queued document is not yet imaged", async () => {
const wrapped = fft.wrap(onAnalysisImageReady);

await getDatabase().ref("demo/AI/portals/demo/aiProcessingQueue/testdoc1").set({
metadataPath: "classes/democlass1/users/1/documentMetadata/testdoc1",
updated: "1001",
status: "updated",
});

const event = {
params: {
docId: "testdoc1",
},
};

await wrapped(event);
expect(logger.info).not.toHaveBeenCalled();
await getDatabase().ref("demo/AI/portals/demo/aiProcessingQueue").once("value", (snapshot) => {
expect(snapshot.val()).toEqual({
testdoc1: {
metadataPath: "classes/democlass1/users/1/documentMetadata/testdoc1",
updated: "1001",
status: "updated",
},
});
});
});

test("creates comment when queued document is imaged", async () => {
const wrapped = fft.wrap(onAnalysisImageReady);

await getDatabase().ref("demo/AI/portals/demo/aiProcessingQueue/testdoc1").set({
metadataPath: "classes/democlass1/users/1/documentMetadata/testdoc1",
updated: "1001",
status: "imaged",
});

const event = {
params: {
docId: "testdoc1",
},
};

await wrapped(event);

expect(logger.info)
.toHaveBeenLastCalledWith("Creating comment for",
"demo/AI/portals/demo/aiProcessingQueue/testdoc1");

await getDatabase().ref("demo/AI/portals/demo/aiProcessingQueue").once("value", (snapshot) => {
const queue = snapshot.val();
expect(Object.keys(queue)).toHaveLength(1);
expect(queue).toEqual({
testdoc1: {
metadataPath: "classes/democlass1/users/1/documentMetadata/testdoc1",
updated: "1001",
status: "done",
},
});
});

firestore.collection("demo/AI/documents/testdoc1/comments").get().then((snapshot) => {
expect(snapshot.size).toBe(1);
const comment = snapshot.docs[0].data();
expect(comment).toEqual({
content: "The document mentions a 'Water Saving Idea' and refers specifically to a 'rain barrel', " +
"indicating a focus on the functionality of the design in terms of saving water. " +
"Key Indicators: Water Saving Idea, rain barrel",
createdAt: expect.any(Object),
name: "Ada Insight",
uid: "ada_insight_1",
});
});
});

test("updates comment when queued document is imaged again", async () => {
const wrapped = fft.wrap(onAnalysisImageReady);

await getDatabase().ref("demo/AI/portals/demo/aiProcessingQueue/testdoc1").set({
metadataPath: "classes/democlass1/users/1/documentMetadata/testdoc1",
updated: "1001",
status: "imaged",
});

await firestore.collection("demo/AI/documents/testdoc1/comments").add({
content: "Old comment",
createdAt: 1,
name: "Ada Insight",
uid: "ada_insight_1",
});
await firestore.collection("demo/AI/documents/testdoc1/comments").get().then((snapshot) => {
expect(snapshot.size).toBe(1);
});

const event = {
params: {
docId: "testdoc1",
},
};

await wrapped(event);

expect(logger.info)
.toHaveBeenLastCalledWith("Updating existing comment for",
"demo/AI/portals/demo/aiProcessingQueue/testdoc1");

await getDatabase().ref("demo/AI/portals/demo/aiProcessingQueue").once("value", (snapshot) => {
const queue = snapshot.val();
expect(Object.keys(queue)).toHaveLength(1);
expect(queue).toEqual({
testdoc1: {
metadataPath: "classes/democlass1/users/1/documentMetadata/testdoc1",
updated: "1001",
status: "done",
},
});
});

await firestore.collection("demo/AI/documents/testdoc1/comments").get().then((snapshot) => {
expect(snapshot.size).toBe(1);
const comment = snapshot.docs[0].data();
expect(comment).toEqual({
content: "The document mentions a 'Water Saving Idea' and refers specifically to a 'rain barrel', " +
"indicating a focus on the functionality of the design in terms of saving water. " +
"Key Indicators: Water Saving Idea, rain barrel",
tags: ["function"],
createdAt: expect.any(Object),
name: "Ada Insight",
uid: "ada_insight_1",
});
expect(comment.createdAt).not.toEqual(1);
});
});
});

afterAll(async () => {
await cleanup();
});
});

0 comments on commit 734bcd7

Please sign in to comment.