Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into preview
Browse files Browse the repository at this point in the history
  • Loading branch information
satococoa committed Jan 29, 2025
2 parents e21398e + 8fce44c commit 47ea39f
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 73 deletions.
79 changes: 41 additions & 38 deletions app/webhooks/github/handle_event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type EmailRecipient, sendEmail } from "@/app/services/email";
import { type agents, db, teamMemberships, users } from "@/drizzle";
import { db } from "@/drizzle";
import { saveAgentActivity } from "@/services/agents/activities";
import { reportAgentTimeUsage } from "@/services/usage-based-billing";
import { executeStep } from "@giselles-ai/lib/execution";
Expand All @@ -11,9 +10,12 @@ import {
import type { Execution, Graph } from "@giselles-ai/types";
import type { Octokit } from "@octokit/core";
import { waitUntil } from "@vercel/functions";
import { eq } from "drizzle-orm";
import { parseCommand } from "./command";
import { assertIssueCommentEvent, createOctokit } from "./utils";
import {
assertIssueCommentEvent,
createOctokit,
notifyWorkflowError,
} from "./utils";

export class WebhookPayloadError extends Error {
constructor(message: string) {
Expand Down Expand Up @@ -52,19 +54,38 @@ export const mockGitHubClientFactory: GitHubClientFactory = {
/**
* Handle GitHub webhook event
* ref: https://docs.github.com/en/webhooks/webhook-events-and-payloads
* currently only supports issue_comment
* @param event
* @returns
*/
export async function handleEvent(
event: { id: string; name: string; payload: unknown },
options?: { githubClientFactory?: GitHubClientFactory },
options: { githubClientFactory: GitHubClientFactory },
): Promise<void> {
const githubClientFactory =
options?.githubClientFactory ?? defaultGitHubClientFactory;
assertIssueCommentEvent(event);
const payload = event.payload;
switch (event.name) {
case "installation":
case "installation_repositories":
// All GitHub Apps receive these events.
console.info(`received event: ${event.name}`);
break;
case "issue_comment":
await handleIssueComment(event, options);
break;
default:
throw new WebhookPayloadError(`Unsupported event name: ${event.name}`);
}
}

async function handleIssueComment(
event: { id: string; name: string; payload: unknown },
options: { githubClientFactory: GitHubClientFactory },
) {
try {
assertIssueCommentEvent(event);
} catch (e: unknown) {
throw new WebhookPayloadError(`Invalid issue comment event: ${e}`);
}

const payload = event.payload;
const command = parseCommand(payload.comment.body);
if (command === null) {
throw new WebhookPayloadError(
Expand All @@ -76,6 +97,8 @@ export async function handleEvent(
`Installation not found. payload: ${JSON.stringify(payload)}`,
);
}

const githubClientFactory = options.githubClientFactory;
const octokit = await githubClientFactory.createClient(
payload.installation.id,
);
Expand Down Expand Up @@ -141,6 +164,14 @@ export async function handleEvent(
nodeId: eventNodeMapping.nodeId,
data: payload.issue.title,
};
case "issue.body":
if (payload.issue.body === null) {
return null;
}
return {
nodeId: eventNodeMapping.nodeId,
data: payload.issue.body,
};
default:
return null;
}
Expand Down Expand Up @@ -192,31 +223,3 @@ export async function handleEvent(
),
);
}

// Notify workflow error to team members
async function notifyWorkflowError(
agent: typeof agents.$inferSelect,
error: string,
) {
const teamMembers = await db
.select({ userDisplayName: users.displayName, userEmail: users.email })
.from(teamMemberships)
.innerJoin(users, eq(teamMemberships.userDbId, users.dbId))
.where(eq(teamMemberships.teamDbId, agent.teamDbId));

if (teamMembers.length === 0) {
return;
}

const subject = `[Giselle] Workflow failure: ${agent.name} (ID: ${agent.id})`;
const body = `Workflow failed with error:
${error}
`.replaceAll("\t", "");

const recipients: EmailRecipient[] = teamMembers.map((user) => ({
userDisplayName: user.userDisplayName ?? "",
userEmail: user.userEmail ?? "",
}));

await sendEmail(subject, body, recipients);
}
31 changes: 31 additions & 0 deletions app/webhooks/github/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { type EmailRecipient, sendEmail } from "@/app/services/email";
import { type agents, db, teamMemberships, users } from "@/drizzle";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/core";
import type { EmitterWebhookEvent } from "@octokit/webhooks";
import { eq } from "drizzle-orm";

export function assertIssueCommentEvent(
payload: unknown,
Expand Down Expand Up @@ -48,3 +51,31 @@ export async function createOctokit(installationId: number | string) {
auth: auth.token,
});
}

// Notify workflow error to team members
export async function notifyWorkflowError(
agent: typeof agents.$inferSelect,
error: string,
) {
const teamMembers = await db
.select({ userDisplayName: users.displayName, userEmail: users.email })
.from(teamMemberships)
.innerJoin(users, eq(teamMemberships.userDbId, users.dbId))
.where(eq(teamMemberships.teamDbId, agent.teamDbId));

if (teamMembers.length === 0) {
return;
}

const subject = `[Giselle] Workflow failure: ${agent.name} (ID: ${agent.id})`;
const body = `Workflow failed with error:
${error}
`.replaceAll("\t", "");

const recipients: EmailRecipient[] = teamMembers.map((user) => ({
userDisplayName: user.userDisplayName ?? "",
userEmail: user.userEmail ?? "",
}));

await sendEmail(subject, body, recipients);
}
75 changes: 40 additions & 35 deletions packages/lib/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,49 +459,18 @@ export async function executeStep({
if (step === undefined) {
throw new Error(`Step with id ${stepId} not found`);
}
const node = graph.nodes.find((node) => node.id === step.nodeId);
if (node === undefined) {
const nodes = applyOverrides(graph.nodes, overrideData);
const executionNode = nodes.find((node) => node.id === step.nodeId);
if (executionNode === undefined) {
throw new Error("Node not found");
}
let executionNode = node;
const overrideDataMap = new Map(
overrideData?.map(({ nodeId, data }) => [nodeId, data]) ?? [],
);
if (overrideDataMap.has(executionNode.id)) {
switch (executionNode.content.type) {
case "textGeneration":
executionNode = {
...executionNode,
content: {
...executionNode.content,
instruction:
overrideDataMap.get(executionNode.id) ??
executionNode.content.instruction,
},
} as Node;
break;
case "text":
executionNode = {
...executeNode,
content: {
...executionNode.content,
text:
overrideDataMap.get(executionNode.id) ??
executionNode.content.text,
},
} as Node;
break;
default:
break;
}
}

const context: ExecutionContext = {
agentId,
executionId,
node: executionNode,
artifacts,
nodes: graph.nodes,
nodes,
connections: graph.connections,
stream,
};
Expand Down Expand Up @@ -626,3 +595,39 @@ async function canPerformFlowExecution(agentId: AgentId) {
const team = res[0];
return await isAgentTimeAvailable(team);
}

function applyOverrides(nodes: Node[], overrideData?: OverrideData[]) {
if (overrideData == null) {
return nodes;
}

const overrideDataMap = new Map(
overrideData.map(({ nodeId, data }) => [nodeId, data]) ?? [],
);
return nodes.map((node) => {
const override = overrideDataMap.get(node.id);
if (override == null) {
return node;
}
switch (node.content.type) {
case "textGeneration":
return {
...node,
content: {
...node.content,
instruction: override,
},
} as Node;
case "text":
return {
...node,
content: {
...node.content,
text: override,
},
} as Node;
default:
throw new Error(`Unsupported override type: ${node.content.type}`);
}
});
}

0 comments on commit 47ea39f

Please sign in to comment.