Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5ab4cb8
Add partner content ingestion pipeline
general-adhoc Jun 5, 2026
74dfda6
Add partner content transcript chunking
general-adhoc Jun 5, 2026
25004fe
Add partner content embedding job
general-adhoc Jun 5, 2026
5c01a49
Tune partner content embedding fanout
general-adhoc Jun 5, 2026
08325cd
Add partner content search, redrive safeguards
general-adhoc Jun 7, 2026
bcd7a1d
normalize partner content schema and ingestion routes
general-adhoc Jun 8, 2026
260d3e3
add draft UI as shared in video 6-8-26
general-adhoc Jun 9, 2026
ff49af1
Merge branch 'main' into partner-natural-lang-search
general-adhoc Jun 17, 2026
467ea80
add titktok and ig support to partner content search
general-adhoc Jun 18, 2026
736c66b
Refactor partner content fetch routes
general-adhoc Jun 18, 2026
40f58fc
Merge branch 'main' into partner-natural-lang-search
general-adhoc Jun 19, 2026
82655f1
rough draft - add reranker, add top content details, add topic fit me…
general-adhoc Jun 20, 2026
d107064
WIP - adjust filters + add performance tracking
general-adhoc Jun 21, 2026
fd0580f
Refactor partner content search route and network filters; add perfor…
general-adhoc Jun 22, 2026
7106cf2
drop redundant indexes
general-adhoc Jun 22, 2026
1faf70f
refactor partner content search route
general-adhoc Jun 22, 2026
926162c
refactor partner content search logic; simplify ranking
general-adhoc Jun 23, 2026
45bb815
refactor partner network UI and content-search into smaller modules
general-adhoc Jun 23, 2026
83337ab
further refactor partner content search; centralize schemas and refac…
general-adhoc Jun 23, 2026
2b9096c
replace ranking-based hydration in content search; parallelize enrich…
general-adhoc Jun 24, 2026
4c91add
switch partner content detail to summary bar (remove vertical content…
general-adhoc Jun 24, 2026
b9b075b
partner content server fetch enhancements
general-adhoc Jun 24, 2026
44dae44
rename code to reflect content summary instead of individual content …
general-adhoc Jun 24, 2026
10a342a
minor language changes on partner detail content page
general-adhoc Jun 24, 2026
4685eac
extend redis embedding cache to 60 days; extend on read. estimated c…
general-adhoc Jun 24, 2026
1dcaaef
Merge branch 'main' into partner-natural-lang-search
steven-tey Jun 28, 2026
91b4500
improve ranking performance to clip to reranker results only; do not …
general-adhoc Jun 24, 2026
42c2598
show full recent content feed on partner detail + share the recent-co…
general-adhoc Jun 28, 2026
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
68 changes: 68 additions & 0 deletions apps/web/app/(ee)/api/admin/partner-content/backfill/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { parseRequestBody } from "@/lib/api/utils";
import { withAdmin } from "@/lib/auth";
import {
createPartnerContentRunStamp,
enqueuePartnerContentEnumerate,
partnerContentIngestionFilterSchema,
} from "@/lib/partner-content-search/ingestion/enqueue";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

export const dynamic = "force-dynamic";
export const maxDuration = 10;

const backfillTriggerSchema = z.object({
filter: partnerContentIngestionFilterSchema,
runStamp: z.string().min(1).optional(),
dryRun: z.boolean().default(false),
dispatcherDryRun: z.boolean().default(false),
});

// POST /api/admin/partner-content/backfill
export const POST = withAdmin(
async ({ req }) => {
try {
// Fail loud on malformed JSON instead of silently widening to a
// full backfill; parseRequestBody throws a 400 DubApiError.
const body = backfillTriggerSchema.parse(await parseRequestBody(req));
const runStamp = body.runStamp ?? createPartnerContentRunStamp();

const payload = {
mode: "backfill" as const,
filter: body.filter,
runStamp,
dryRun: body.dispatcherDryRun,
};

if (body.dryRun) {
return NextResponse.json({
success: true,
triggerDryRun: true,
dispatcherDryRun: payload.dryRun,
wouldEnqueue: false,
enumeratePayload: payload,
});
}

const qstashResponse = await enqueuePartnerContentEnumerate(payload);

return NextResponse.json({
success: true,
triggerDryRun: false,
dispatcherDryRun: payload.dryRun,
wouldEnqueue: true,
runStamp,
enumeratePayload: payload,
qstashResponse,
});
} catch (error) {
// Normalize ZodError -> 422, DubApiError -> 4xx, QStash failure -> 500
// (the withAdmin wrapper does not do this for us).
return handleAndReturnErrorResponse(error);
}
},
{
requiredRoles: ["owner"],
},
);
126 changes: 126 additions & 0 deletions apps/web/app/(ee)/api/admin/partner-content/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
import { parseRequestBody } from "@/lib/api/utils";
import { withAdmin } from "@/lib/auth";
import {
PARTNER_CONTENT_SEARCH_LIMITS,
PARTNER_CONTENT_SEARCH_MODELS,
PARTNER_CONTENT_SEARCH_VOYAGE_QUERY_TIMEOUT_MS,
} from "@/lib/partner-content-search/constants";
import { searchAdminPartnerContentChunks } from "@/lib/partner-content-search/retrieval";
import {
groupPartnerSearchResults,
rerankPartnerSearchRows,
toScore,
type PartnerContentSearchRow,
} from "@/lib/partner-content-search/search-utils";
import {
embedPartnerContentTexts,
serializeEmbeddingForVector,
VoyageTimeoutError,
} from "@/lib/partner-content-search/voyage";
import { partnerAdminContentSearchSchema } from "@/lib/zod/schemas/partner-network";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";
export const maxDuration = 30;

// POST /api/admin/partner-content/search
export const POST = withAdmin(
async ({ req }) => {
try {
const body = partnerAdminContentSearchSchema.parse(
await parseRequestBody(req),
);
const candidateChunkCount =
body.candidateChunkCount ??
Math.min(
PARTNER_CONTENT_SEARCH_LIMITS.chunkCandidateCount,
Math.max(25, body.limit * body.chunksPerPartner * 5),
);

let queryEmbedding: number[];
try {
[queryEmbedding] = await embedPartnerContentTexts({
input: [body.query],
inputType: "query",
timeoutMs: PARTNER_CONTENT_SEARCH_VOYAGE_QUERY_TIMEOUT_MS,
});
} catch (error) {
if (error instanceof VoyageTimeoutError) {
throw new DubApiError({
code: "internal_server_error",
message: "Partner content search timed out. Please try again.",
});
}
throw error;
}

const queryVector = serializeEmbeddingForVector(queryEmbedding);
const candidateRows = await searchAdminPartnerContentChunks({
queryVector,
limit: candidateChunkCount,
partnerIds: body.partnerIds,
platform: body.platform,
});
const { rows, reranked } = body.rerank
? await rerankPartnerSearchRows({
query: body.query,
rows: candidateRows,
})
: { rows: candidateRows, reranked: false };

return NextResponse.json({
success: true,
query: body.query,
candidateChunkCount,
embeddingModel: PARTNER_CONTENT_SEARCH_MODELS.embedding.id,
reranked,
rerankModel: reranked
? PARTNER_CONTENT_SEARCH_MODELS.reranker.model
: null,
resultCount: rows.length,
partners: groupPartnerSearchResults({
rows,
limit: body.limit,
chunksPerPartner: body.chunksPerPartner,
toChunkResult,
}),
});
} catch (error) {
return handleAndReturnErrorResponse(error);
}
},
{
requiredRoles: ["owner"],
},
);

function toChunkResult(row: PartnerContentSearchRow, distance: number) {
return {
chunkId: row.chunkId,
partnerContentItemId: row.partnerContentItemId,
platform: {
type: row.platformType,
identifier: row.platformIdentifier,
},
content: {
platformContentId: row.platformContentId,
url: row.contentUrl,
title: row.contentTitle,
thumbnailUrl: row.contentThumbnailUrl,
publishedAt: row.contentPublishedAt?.toISOString() ?? null,
durationMs: row.contentDurationMs,
},
chunk: {
source: row.chunkSource,
index: row.chunkIndex,
chunkText: row.chunkText,
startMs: row.startMs,
endMs: row.endMs,
},
distance,
score: row.rerankScore ?? toScore(distance),
cosineScore: toScore(distance),
rerankScore: row.rerankScore ?? null,
};
}
Loading
Loading