From 83685a96f845988e34bdb975b274727a36a75df8 Mon Sep 17 00:00:00 2001 From: jennmueng Date: Tue, 3 Dec 2024 23:46:10 -0800 Subject: [PATCH 1/2] editing init --- .../src/components/prDrafts/DraftingTitle.tsx | 14 ++- .../views/navigation/PendingPullRequests.tsx | 2 +- apps/app/src/views/prDetail/PrDetailView.tsx | 2 +- apps/app/src/views/prDetail/hooks.ts | 14 ++- .../app/api/ai/create-draft-for-pr/index.ts | 119 +++++++++++++----- packages/db/src/database.types.ts | 3 + packages/utils/src/common/git.ts | 5 + packages/utils/src/common/library.ts | 2 +- 8 files changed, 125 insertions(+), 36 deletions(-) diff --git a/apps/app/src/components/prDrafts/DraftingTitle.tsx b/apps/app/src/components/prDrafts/DraftingTitle.tsx index 9abc7d8..4e9a576 100644 --- a/apps/app/src/components/prDrafts/DraftingTitle.tsx +++ b/apps/app/src/components/prDrafts/DraftingTitle.tsx @@ -1,4 +1,4 @@ -import { DocumentPrDraftRecord, PrDraftDocumentStatus } from "@cloudy/utils/common"; +import { DocumentPrDraftRecord, PrDraftDocumentModificationType, PrDraftDocumentStatus } from "@cloudy/utils/common"; import { BookDashedIcon, FileCheckIcon, XIcon } from "lucide-react"; export const DraftingTitle = ({ documentDraft }: { documentDraft: DocumentPrDraftRecord }) => { @@ -11,10 +11,20 @@ export const DraftingTitle = ({ documentDraft }: { documentDraft: DocumentPrDraf [PrDraftDocumentStatus.SKIPPED]: , }; + const prefixStates = { + [PrDraftDocumentModificationType.CREATE]: "Creating", + [PrDraftDocumentModificationType.EDIT]: "Editing", + }; + return (
{iconStates[status]}
- {documentDraft?.path} +
+ + {prefixStates[documentDraft.modification_type as PrDraftDocumentModificationType] + " "} + + {documentDraft?.path} +
); }; diff --git a/apps/app/src/views/navigation/PendingPullRequests.tsx b/apps/app/src/views/navigation/PendingPullRequests.tsx index 4a8d442..ee0a39f 100644 --- a/apps/app/src/views/navigation/PendingPullRequests.tsx +++ b/apps/app/src/views/navigation/PendingPullRequests.tsx @@ -24,7 +24,7 @@ export const useOpenPrs = () => { await supabase .from("pull_request_metadata") .select( - "id, pr_number, pr_status, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts(id, title, content_md))", + "id, pr_number, pr_status, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts!document_id(id, title, content_md))", ) .eq("project_id", project!.id), ); diff --git a/apps/app/src/views/prDetail/PrDetailView.tsx b/apps/app/src/views/prDetail/PrDetailView.tsx index df32b5f..ffedcdb 100644 --- a/apps/app/src/views/prDetail/PrDetailView.tsx +++ b/apps/app/src/views/prDetail/PrDetailView.tsx @@ -99,7 +99,7 @@ export const PrDetailView = () => { }; return ( -
+
diff --git a/apps/app/src/views/prDetail/hooks.ts b/apps/app/src/views/prDetail/hooks.ts index 5fdad02..e115a95 100644 --- a/apps/app/src/views/prDetail/hooks.ts +++ b/apps/app/src/views/prDetail/hooks.ts @@ -1,4 +1,4 @@ -import { PrDocsStatus, PrDraftDocumentStatus, handleSupabaseError } from "@cloudy/utils/common"; +import { PrDocsStatus, PrDraftDocumentStatus, fixOneToOne, handleSupabaseError } from "@cloudy/utils/common"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useParams } from "react-router-dom"; @@ -83,15 +83,23 @@ export const usePrDetail = () => { return useQuery({ queryKey: prQueryKeys.prDetail(prMetadataId!), queryFn: async () => { - return handleSupabaseError( + const data = handleSupabaseError( await supabase .from("pull_request_metadata") .select( - "*, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts(id, title, content_md))", + "*, repo:repository_connections(owner,name), document_pr_drafts(*, document:thoughts!document_id(id, title, content_md))", ) .eq("id", prMetadataId!) .single(), ); + + return { + ...data, + document_pr_drafts: data.document_pr_drafts.map(draft => ({ + ...draft, + document: fixOneToOne(draft.document), + })), + }; }, }); }; diff --git a/apps/web/app/api/ai/create-draft-for-pr/index.ts b/apps/web/app/api/ai/create-draft-for-pr/index.ts index 76612fc..4fbf02a 100644 --- a/apps/web/app/api/ai/create-draft-for-pr/index.ts +++ b/apps/web/app/api/ai/create-draft-for-pr/index.ts @@ -1,4 +1,6 @@ import { + LibraryItem, + PrDraftDocumentModificationType, PrStatus, RepositoryConnectionRecord, getLibraryItems, @@ -8,6 +10,7 @@ import { zip, } from "@cloudy/utils/common"; import { Database } from "@repo/db"; +import * as Sentry from "@sentry/nextjs"; import { SupabaseClient } from "@supabase/supabase-js"; import { generateObject, generateText, tool } from "ai"; import { z } from "zod"; @@ -28,14 +31,28 @@ const documentSchema = z.object({ .string() .describe('The path to the document within the library, for example "/folder1/Folder 2/Document title here"'), title: z.string().describe("The title of the document"), - content: z.string().describe("The markdown contents of the document"), + content: z.string().describe("The entire markdown contents of the document"), + type: z.enum(["create", "edit"]).describe("Whether to create or edit an existing document"), }); -type Document = z.infer; +type DocumentEdit = z.infer; -const getLibraryAsPrompt = async (workspaceId: string, projectId: string, supabase: SupabaseClient) => { - const libraryItems = await getLibraryItems({ workspaceId, projectId }, supabase); +const getDocumentAsPrompt = async (documentId: string, supabase: SupabaseClient) => { + const document = handleSupabaseError( + await supabase.from("thoughts").select("title, content_md").eq("id", documentId).single(), + ); + + return ` + +${document.title} + + +${document.content_md} + +`; +}; +const getLibraryAsPrompt = async (libraryItems: LibraryItem[]) => { if (libraryItems.length === 0) { return `There currently isn't any existing documentation for this project.`; } @@ -50,16 +67,14 @@ ${libraryItems.map(item => `- ${item.type} ID "${item.id.slice(0, 6)}": "${item. const makeSystemPrompt = () => { return `You are an expert at creating documentation for projects. You are able to create folders/paths and markdown documents. - You can create documents in the root of the library ("/Document name") or in any subfolder: ("/path/to/folder/Document title here"). -- You do not need to create a /docs folder, the root folder is already for docs.`; +- You do not need to create a /docs folder, the root folder is already for docs. +- You have tools to view existing documents and to create/edit documents. + +Create effective documentation for a whole project's codebase, not just for the pull request.`; }; -const makePrDocsGenerationPrompt = async ( - payload: PullRequestDocsGenerationDetails, - workspaceId: string, - projectId: string, - supabase: SupabaseClient, -) => { - return `${await getLibraryAsPrompt(workspaceId, projectId, supabase)} +const makePrDocsGenerationPrompt = async (payload: PullRequestDocsGenerationDetails, libraryItems: LibraryItem[]) => { + return `${await getLibraryAsPrompt(libraryItems)} - You don't need to provide a h1 title again in the document contents, place the title in the title field. @@ -126,6 +141,27 @@ export const createDraftForPr = async ( const diffText = comparison.data.files?.map(file => file.patch).join("\n\n") ?? ""; + if (diffText.length > 60000) { + await octokit.rest.issues.createComment({ + owner: repositoryConnection.owner, + repo: repositoryConnection.name, + issue_number: pullRequestNumber, + body: `👋 Looks like your changes are too big to generate docs for, I'm skipping this PR. We're working on supporting larger diffs soon!`, + }); + + console.log("Diff text is too long, currently not supported"); + + Sentry.captureMessage("Diff text is too long, currently not supported", { + extra: { + workspace, + repositoryConnectionId: repositoryConnection.id, + pullRequestNumber, + }, + }); + + return; + } + const { object: { needsDocs }, } = await generateObject({ @@ -145,33 +181,58 @@ export const createDraftForPr = async ( body: `👋 Looks like your changes don't need any docs, you're all clear!`, }); + console.log("No docs needed"); + return; } - // 1. Determine whether the pr needs any docs - // 2. Generate the docs, we'll need to support as many pages as needed - // 2.1. Also, ideally we get a folder structure for the docs - // 3. Somehow we also make these docs only draft documents, that get deleted when the PR is closed and published when the PR is merged + const libraryItems = await getLibraryItems({ workspaceId: workspace.id, projectId: project.id }, supabase); + + const libraryPathsToItems = Object.fromEntries(libraryItems.map(item => [item.path, item])); - const documents: Document[] = []; + const documents: DocumentEdit[] = []; const { text } = await generateText({ model: heliconeAnthropic.languageModel("claude-3-5-haiku-20241022"), system: makeSystemPrompt(), - prompt: await makePrDocsGenerationPrompt( - { title, description: description ?? "", diffText }, - workspace.id, - project.id, - supabase, - ), + prompt: await makePrDocsGenerationPrompt({ title, description: description ?? "", diffText }, libraryItems), tools: { - createDocument: tool({ - description: "Create a document", + viewDocuments: tool({ + description: "View documents", + parameters: z.object({ + paths: z.array(z.string()).describe("The paths to the documents within the library"), + }), + execute: async ({ paths }) => { + return await Promise.all( + paths.map(async path => { + const item = libraryPathsToItems[path]; + + if (!item) { + return `Error: ${path} is not a valid path`; + } + + if (item.type === "folder") { + return `Error: ${item.path} is a folder, not a document`; + } + + return await getDocumentAsPrompt(item.id, supabase); + }), + ); + }, + }), + modifyDocument: tool({ + description: "Edit or create a document", parameters: documentSchema, - execute: async ({ path, title, content }) => { - documents.push({ path, title, content }); + execute: async ({ path, title, content, type }) => { + if (type === "edit" && !libraryPathsToItems[path]) { + return `Error: ${path} is not a valid path, cannot edit`; + } else if (type === "create" && libraryPathsToItems[path]) { + return `Error: ${path} is already a document, cannot create`; + } + + documents.push({ path, title, content, type }); - return "Document created"; + return "Done"; }, }), }, @@ -213,6 +274,8 @@ export const createDraftForPr = async ( pr_metadata_id: prMetadata.id, document_id: documentId, path: document.path, + type: document.type as PrDraftDocumentModificationType, + original_document_id: document.type === "create" ? null : libraryPathsToItems[document.path]!.id, })), ) .select("id"), diff --git a/packages/db/src/database.types.ts b/packages/db/src/database.types.ts index d8f3a83..620c353 100644 --- a/packages/db/src/database.types.ts +++ b/packages/db/src/database.types.ts @@ -953,6 +953,7 @@ export type Database = { created_at: string document_id: string id: string + modification_type: string path: string | null pr_metadata_id: string status: string @@ -961,6 +962,7 @@ export type Database = { created_at?: string document_id: string id?: string + modification_type?: string path?: string | null pr_metadata_id: string status?: string @@ -969,6 +971,7 @@ export type Database = { created_at?: string document_id?: string id?: string + modification_type?: string path?: string | null pr_metadata_id?: string status?: string diff --git a/packages/utils/src/common/git.ts b/packages/utils/src/common/git.ts index 12232d7..392dc04 100644 --- a/packages/utils/src/common/git.ts +++ b/packages/utils/src/common/git.ts @@ -17,6 +17,11 @@ export enum PrDraftDocumentStatus { SKIPPED = "skipped", } +export enum PrDraftDocumentModificationType { + CREATE = "create", + EDIT = "edit", +} + export const makeGithubPrUrl = ( owner: string, repo: string, diff --git a/packages/utils/src/common/library.ts b/packages/utils/src/common/library.ts index c586fd0..885b423 100644 --- a/packages/utils/src/common/library.ts +++ b/packages/utils/src/common/library.ts @@ -127,7 +127,7 @@ export const getLibraryItems = async ( title, created_at ), - document_pr_drafts( + document_pr_drafts!document_id( id ) ` From c4a85e15866a437dd636e10c572967eb48ace4df Mon Sep 17 00:00:00 2001 From: jennmueng Date: Tue, 3 Dec 2024 23:46:17 -0800 Subject: [PATCH 2/2] dev route --- .../app/api/ai/create-draft-for-pr/route.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/web/app/api/ai/create-draft-for-pr/route.ts diff --git a/apps/web/app/api/ai/create-draft-for-pr/route.ts b/apps/web/app/api/ai/create-draft-for-pr/route.ts new file mode 100644 index 0000000..05ae473 --- /dev/null +++ b/apps/web/app/api/ai/create-draft-for-pr/route.ts @@ -0,0 +1,27 @@ +import { PrStatus, handleSupabaseError } from "@cloudy/utils/common"; +import { NextRequest, NextResponse } from "next/server"; + +import { getSupabase } from "app/api/utils/supabase"; + +import { createDraftForPr } from "."; + +//** Use only for testing */ +export const POST = async (req: NextRequest) => { + const supabase = await getSupabase({ mode: "service", bypassAuth: true }); + const repositoryConnection = handleSupabaseError( + await supabase.from("repository_connections").select("*").eq("id", "ca076ea0-4d17-47ff-a4a3-851027c839b6").single(), + ); + const diffText = await createDraftForPr( + repositoryConnection, + 36, + "feat(pages): Introduce public library v0", + "Allows you to partially make any document/folder of your library public on cloudy pages.", + PrStatus.OPEN, + "325946f67d701b42a9fbb1a09efb79c02b5c0364", + "abf4bd2759bc0789b9ec7e475387ccb8cda2216f", + ); + + return NextResponse.json({ + diffText, + }); +};