-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e157f38
commit 734bcd7
Showing
4 changed files
with
252 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |