Add agent workflows & stuff
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
'use node';
|
||||
|
||||
import { getAuthUserId } from '@convex-dev/auth/server';
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { ActionCtx } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import { action } from './_generated/server';
|
||||
import { reviewUpstreamCompatibility } from './openaiClient';
|
||||
import { decryptSecret } from './secretCrypto';
|
||||
|
||||
const getRequiredUserId = async (ctx: ActionCtx): Promise<Id<'users'>> => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new ConvexError('Not authenticated.');
|
||||
return userId;
|
||||
};
|
||||
|
||||
export const reviewLatestUpstreamChanges = action({
|
||||
args: { spoonId: v.id('spoons') },
|
||||
handler: async (
|
||||
ctx,
|
||||
{ spoonId },
|
||||
): Promise<{
|
||||
reviewId: Id<'aiReviews'>;
|
||||
risk: 'low' | 'medium' | 'high';
|
||||
recommendedAction:
|
||||
| 'sync'
|
||||
| 'open_review_pr'
|
||||
| 'manual_review'
|
||||
| 'do_not_sync';
|
||||
}> => {
|
||||
const ownerId = await getRequiredUserId(ctx);
|
||||
const spoon: Doc<'spoons'> = await ctx.runQuery(
|
||||
internal.spoons.getOwnedForAction,
|
||||
{
|
||||
spoonId,
|
||||
ownerId,
|
||||
},
|
||||
);
|
||||
const [state, settings, upstreamCommits, forkCommits]: [
|
||||
Doc<'spoonRepositoryStates'> | null,
|
||||
Doc<'spoonSettings'> | null,
|
||||
Doc<'spoonCommits'>[],
|
||||
Doc<'spoonCommits'>[],
|
||||
] = await Promise.all([
|
||||
ctx.runQuery(internal.spoonState.getInternal, { spoonId, ownerId }),
|
||||
ctx.runQuery(internal.spoonSettings.getInternal, { spoonId, ownerId }),
|
||||
ctx.runQuery(internal.spoonCommits.listInternal, {
|
||||
spoonId,
|
||||
ownerId,
|
||||
side: 'upstream',
|
||||
limit: 80,
|
||||
}),
|
||||
ctx.runQuery(internal.spoonCommits.listInternal, {
|
||||
spoonId,
|
||||
ownerId,
|
||||
side: 'fork',
|
||||
limit: 80,
|
||||
}),
|
||||
]);
|
||||
const aiSettings: Doc<'userAiSettings'> | null = await ctx.runQuery(
|
||||
internal.aiSettings.getForUserInternal,
|
||||
{ userId: ownerId },
|
||||
);
|
||||
if (!aiSettings?.encryptedApiKey) {
|
||||
throw new ConvexError(
|
||||
'Add your OpenAI API key in Settings before running AI review.',
|
||||
);
|
||||
}
|
||||
const model = aiSettings.model;
|
||||
const syncRunId: Id<'syncRuns'> = await ctx.runMutation(
|
||||
internal.syncRuns.createInternal,
|
||||
{
|
||||
spoonId,
|
||||
ownerId,
|
||||
kind: 'ai_review',
|
||||
status: 'running',
|
||||
summary: 'Reviewing upstream changes with OpenAI.',
|
||||
},
|
||||
);
|
||||
const reviewId: Id<'aiReviews'> = await ctx.runMutation(
|
||||
internal.aiReviews.createInternal,
|
||||
{
|
||||
spoonId,
|
||||
ownerId,
|
||||
syncRunId,
|
||||
model,
|
||||
status: 'running',
|
||||
reviewType: 'upstream_update',
|
||||
inputSummary: `${upstreamCommits.length} upstream commit(s), ${forkCommits.length} fork-only commit(s).`,
|
||||
},
|
||||
);
|
||||
try {
|
||||
if (upstreamCommits.length === 0) {
|
||||
await ctx.runMutation(internal.aiReviews.completeInternal, {
|
||||
reviewId,
|
||||
outputSummary: 'The fork is already up to date with upstream.',
|
||||
risk: 'low',
|
||||
compatible: true,
|
||||
requiresHumanReview: false,
|
||||
recommendedAction: 'sync',
|
||||
potentialConflicts: [],
|
||||
importantFiles: [],
|
||||
reasoningSummary:
|
||||
'No upstream-only commits are cached for this Spoon, so there is nothing to review.',
|
||||
});
|
||||
await Promise.all([
|
||||
ctx.runMutation(internal.spoons.patchSyncFields, {
|
||||
spoonId,
|
||||
lastAiReviewId: reviewId,
|
||||
}),
|
||||
ctx.runMutation(internal.syncRuns.patchInternal, {
|
||||
syncRunId,
|
||||
status: 'clean',
|
||||
aiAssessment: 'No upstream-only commits are waiting.',
|
||||
}),
|
||||
]);
|
||||
return { reviewId, risk: 'low', recommendedAction: 'sync' };
|
||||
}
|
||||
|
||||
const review = await reviewUpstreamCompatibility(
|
||||
{
|
||||
spoonName: spoon.name,
|
||||
upstreamFullName:
|
||||
state?.upstreamFullName ??
|
||||
`${spoon.upstreamOwner}/${spoon.upstreamRepo}`,
|
||||
forkFullName:
|
||||
state?.forkFullName ??
|
||||
`${spoon.forkOwner ?? 'unknown'}/${spoon.forkRepo ?? 'unknown'}`,
|
||||
status: state?.status ?? spoon.syncStatus ?? 'unknown',
|
||||
upstreamAheadBy: state?.upstreamAheadBy ?? spoon.upstreamAheadBy ?? 0,
|
||||
forkAheadBy: state?.forkAheadBy ?? spoon.forkAheadBy ?? 0,
|
||||
upstreamCommits: upstreamCommits.map((commit) => ({
|
||||
sha: commit.sha,
|
||||
message: commit.message,
|
||||
authorName: commit.authorName,
|
||||
committedAt: commit.committedAt,
|
||||
})),
|
||||
forkCommits: forkCommits.map((commit) => ({
|
||||
sha: commit.sha,
|
||||
message: commit.message,
|
||||
authorName: commit.authorName,
|
||||
committedAt: commit.committedAt,
|
||||
})),
|
||||
importantFilePatterns: settings?.importantFilePatterns,
|
||||
ignoredFilePatterns: settings?.ignoredFilePatterns,
|
||||
},
|
||||
{
|
||||
apiKey: decryptSecret(aiSettings.encryptedApiKey),
|
||||
model,
|
||||
reasoningEffort: aiSettings.reasoningEffort,
|
||||
},
|
||||
);
|
||||
await ctx.runMutation(internal.aiReviews.completeInternal, {
|
||||
reviewId,
|
||||
outputSummary: review.summary,
|
||||
risk: review.risk,
|
||||
compatible: review.compatible,
|
||||
requiresHumanReview: review.requiresHumanReview,
|
||||
recommendedAction: review.recommendedAction,
|
||||
potentialConflicts: review.potentialConflicts,
|
||||
importantFiles: review.importantFiles,
|
||||
reasoningSummary: review.reasoningSummary,
|
||||
});
|
||||
await Promise.all([
|
||||
ctx.runMutation(internal.spoons.patchSyncFields, {
|
||||
spoonId,
|
||||
lastAiReviewId: reviewId,
|
||||
}),
|
||||
ctx.runMutation(internal.syncRuns.patchInternal, {
|
||||
syncRunId,
|
||||
status:
|
||||
review.compatible && review.risk === 'low'
|
||||
? 'clean'
|
||||
: 'needs_review',
|
||||
aiAssessment: review.summary,
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
reviewId,
|
||||
risk: review.risk,
|
||||
recommendedAction: review.recommendedAction,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await Promise.all([
|
||||
ctx.runMutation(internal.aiReviews.failInternal, {
|
||||
reviewId,
|
||||
error: message,
|
||||
}),
|
||||
ctx.runMutation(internal.syncRuns.patchInternal, {
|
||||
syncRunId,
|
||||
status: 'failed',
|
||||
error: message,
|
||||
}),
|
||||
]);
|
||||
throw new ConvexError(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user