Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions junction-app/app/api/reports/[id]/ask/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ai } from "@/lib/gemini";
import { adminDb } from "@/lib/firebase-admin";

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const { question } = await request.json();

if (!id) {
return Response.json({ error: "Report ID is required" }, { status: 400 });
}

if (!question || typeof question !== "string") {
return Response.json(
{ error: "Question is required and must be a string" },
{ status: 400 },
);
}

const cacheDoc = await adminDb
.collection("cache")
.doc(id.toLowerCase())
.get();

if (!cacheDoc.exists) {
return Response.json(
{ error: "Report not found", id: id.toLowerCase() },
{ status: 404 },
);
}

const docData = cacheDoc.data();
const contextPayload = {
id: cacheDoc.id,
cached_at: docData?.cached_at ?? null,
query: docData?.query ?? id,
report: docData?.report ?? null,
};

const result = await ai.models.generateContent({
model: "gemini-2.0-flash-exp",
contents: [
{
role: "user",
parts: [
{
text: `You are an expert security analyst. Rely ONLY on the context provided below when answering the user's question.
If the context does not contain the requested information, reply exactly with "I don't have information about that in this report."
Do not speculate, hallucinate, or reference external knowledge. Keep answers to 2-4 concise sentences and reference concrete values from the context when available.

Context (JSON):
${JSON.stringify(contextPayload, null, 2)}

Question: ${question}`,
},
],
},
],
});

const answer = result.text?.trim();

if (!answer) {
return Response.json(
{ error: "The analyst could not generate an answer." },
{ status: 502 },
);
}

return Response.json({
answer,
reportId: cacheDoc.id,
question,
});
} catch (error) {
console.error("Error answering report question:", error);
return Response.json(
{
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
);
}
}
54 changes: 43 additions & 11 deletions junction-app/app/api/research/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,27 @@ Entity name or hash:`;
);
}

// Step 2: Query Firestore for existing entity (case-insensitive fuzzy match)
// Step 2: Check if the report already exists in cache
const entityNameLower = entityName.toLowerCase();
const cacheCollection = adminDb.collection("cache");
const directCacheDoc = await cacheCollection.doc(entityNameLower).get();

if (directCacheDoc.exists) {
const cachedData = directCacheDoc.data();
return Response.json({
found: true,
reportId: directCacheDoc.id,
cachedAt: cachedData?.cached_at || null,
entityName,
});
}

// Step 3: Query Firestore entities for case-insensitive fuzzy match
const entitiesRef = adminDb.collection("entities");
const snapshot = await entitiesRef.get();

let matchedEntity = null;
const entityNameLower = entityName.toLowerCase();
let matchedEntity: Record<string, any> | null = null;

// Try exact match first, then fuzzy match
for (const doc of snapshot.docs) {
const data = doc.data();
const storedName = data.name?.toLowerCase() || "";
Expand All @@ -87,16 +100,35 @@ Entity name or hash:`;
}
}

// Step 3: If entity exists, return it
// Step 4: If entity maps to a cached report, return it
if (matchedEntity) {
return Response.json({
found: true,
entity: matchedEntity,
entityName,
});
const possibleCacheIds = [
matchedEntity.cacheId,
matchedEntity.cache_id,
matchedEntity.cacheDocId,
matchedEntity.cache_doc_id,
matchedEntity.id,
matchedEntity.name,
]
.filter(Boolean)
.map((value: string) => value.toLowerCase());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runtime TypeError when non-string values are processed. The code filters possibleCacheIds from matchedEntity properties (cacheId, cache_id, id, name, etc.) using .filter(Boolean), which only removes falsy values but does NOT guarantee the remaining values are strings. Since matchedEntity is typed as Record<string, any> and populated from untyped Firestore data, these fields could be numbers, objects, arrays, or other non-string types. The type annotation (value: string) is merely a type assertion and does not perform runtime validation. When .toLowerCase() is called on a non-string value, it will throw: TypeError: value.toLowerCase is not a function. This will crash the API endpoint when triggered by Firestore data containing non-string cache ID fields. To fix, convert values to strings before calling toLowerCase(): .map((value) => String(value).toLowerCase())


React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)


for (const cacheId of possibleCacheIds) {
const cacheDoc = await cacheCollection.doc(cacheId).get();
if (cacheDoc.exists) {
const cachedData = cacheDoc.data();
return Response.json({
found: true,
reportId: cacheDoc.id,
cachedAt: cachedData?.cached_at || null,
entityName,
entity: matchedEntity,
});
}
}
}

// Step 4: Entity not found, return entity name for client to trigger deep_security
// Step 5: Entity not found in cache, return entity name for client to trigger deep_security
// The client will call the deep_security endpoint
return Response.json({
found: false,
Expand Down
Loading