[WEB-1559] chore: make AI assistant endpoint workspace level (#4770)

* chore: make ai assistant endpoint workspace level

* chore: create workspace level ai assistant endpoint
This commit is contained in:
Aaryan Khandelwal 2024-06-12 15:01:53 +05:30 committed by GitHub
parent f4ceaaf01c
commit aa92ace57f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 90 additions and 41 deletions

View File

@ -2,7 +2,7 @@ from django.urls import path
from plane.app.views import UnsplashEndpoint from plane.app.views import UnsplashEndpoint
from plane.app.views import GPTIntegrationEndpoint from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpoint
urlpatterns = [ urlpatterns = [
@ -16,4 +16,9 @@ urlpatterns = [
GPTIntegrationEndpoint.as_view(), GPTIntegrationEndpoint.as_view(),
name="importer", name="importer",
), ),
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",
),
] ]

View File

@ -186,6 +186,7 @@ from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .external.base import ( from .external.base import (
GPTIntegrationEndpoint, GPTIntegrationEndpoint,
UnsplashEndpoint, UnsplashEndpoint,
WorkspaceGPTIntegrationEndpoint,
) )
from .estimate.base import ( from .estimate.base import (
ProjectEstimatePointEndpoint, ProjectEstimatePointEndpoint,

View File

@ -11,7 +11,7 @@ from rest_framework import status
# Module imports # Module imports
from ..base import BaseAPIView from ..base import BaseAPIView
from plane.app.permissions import ProjectEntityPermission from plane.app.permissions import ProjectEntityPermission, WorkspaceEntityPermission
from plane.db.models import Workspace, Project from plane.db.models import Workspace, Project
from plane.app.serializers import ( from plane.app.serializers import (
ProjectLiteSerializer, ProjectLiteSerializer,
@ -83,6 +83,64 @@ class GPTIntegrationEndpoint(BaseAPIView):
) )
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def post(self, request, slug):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
[
{
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", None),
},
{
"key": "GPT_ENGINE",
"default": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
},
]
)
# Get the configuration value
# Check the keys
if not OPENAI_API_KEY or not GPT_ENGINE:
return Response(
{"error": "OpenAI API key and engine is required"},
status=status.HTTP_400_BAD_REQUEST,
)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
if not task:
return Response(
{"error": "Task is required"},
status=status.HTTP_400_BAD_REQUEST,
)
final_text = task + "\n" + prompt
client = OpenAI(
api_key=OPENAI_API_KEY,
)
response = client.chat.completions.create(
model=GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
},
status=status.HTTP_200_OK,
)
class UnsplashEndpoint(BaseAPIView): class UnsplashEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
(UNSPLASH_ACCESS_KEY,) = get_configuration_value( (UNSPLASH_ACCESS_KEY,) = get_configuration_value(

View File

@ -97,20 +97,17 @@ const PageDetailsPage = observer(() => {
<PageHead title={name} /> <PageHead title={name} />
<div className="flex h-full flex-col justify-between"> <div className="flex h-full flex-col justify-between">
<div className="h-full w-full flex-shrink-0 flex flex-col overflow-hidden"> <div className="h-full w-full flex-shrink-0 flex flex-col overflow-hidden">
{projectId && ( <PageEditorHeaderRoot
<PageEditorHeaderRoot editorRef={editorRef}
editorRef={editorRef} readOnlyEditorRef={readOnlyEditorRef}
readOnlyEditorRef={readOnlyEditorRef} editorReady={editorReady}
editorReady={editorReady} readOnlyEditorReady={readOnlyEditorReady}
readOnlyEditorReady={readOnlyEditorReady} handleDuplicatePage={handleDuplicatePage}
handleDuplicatePage={handleDuplicatePage} markings={markings}
markings={markings} page={page}
page={page} sidePeekVisible={sidePeekVisible}
projectId={projectId.toString()} setSidePeekVisible={(state) => setSidePeekVisible(state)}
sidePeekVisible={sidePeekVisible} />
setSidePeekVisible={(state) => setSidePeekVisible(state)}
/>
)}
<PageEditorBody <PageEditorBody
editorRef={editorRef} editorRef={editorRef}
handleEditorReady={(val) => setEditorReady(val)} handleEditorReady={(val) => setEditorReady(val)}

View File

@ -5,19 +5,17 @@ import { Placement } from "@popperjs/core";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form"; // services import { Controller, useForm } from "react-hook-form"; // services
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
// ui
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
// icons
// components // components
// hooks import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
// services
import { AIService } from "@/services/ai.service"; import { AIService } from "@/services/ai.service";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
projectId: string;
handleClose: () => void; handleClose: () => void;
onResponse: (response: any) => void; onResponse: (response: any) => void;
onError?: (error: any) => void; onError?: (error: any) => void;
@ -35,7 +33,7 @@ type FormData = {
const aiService = new AIService(); const aiService = new AIService();
export const GptAssistantPopover: React.FC<Props> = (props) => { export const GptAssistantPopover: React.FC<Props> = (props) => {
const { isOpen, projectId, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props; const { isOpen, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props;
// states // states
const [response, setResponse] = useState(""); const [response, setResponse] = useState("");
const [invalidResponse, setInvalidResponse] = useState(false); const [invalidResponse, setInvalidResponse] = useState(false);
@ -88,7 +86,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
const callAIService = async (formData: FormData) => { const callAIService = async (formData: FormData) => {
try { try {
const res = await aiService.createGptTask(workspaceSlug as string, projectId, { const res = await aiService.createGptTask(workspaceSlug.toString(), {
prompt: prompt || "", prompt: prompt || "",
task: formData.task, task: formData.task,
}); });
@ -111,7 +109,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
}; };
const handleAIResponse = async (formData: FormData) => { const handleAIResponse = async (formData: FormData) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug) return;
if (formData.task === "") { if (formData.task === "") {
handleInvalidTask(); handleInvalidTask();

View File

@ -239,7 +239,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
setIAmFeelingLucky(true); setIAmFeelingLucky(true);
aiService aiService
.createGptTask(workspaceSlug.toString(), projectId, { .createGptTask(workspaceSlug.toString(), {
prompt: issueName, prompt: issueName,
task: "Generate a proper description for this issue.", task: "Generate a proper description for this issue.",
}) })
@ -489,7 +489,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
{config?.has_openai_configured && projectId && ( {config?.has_openai_configured && projectId && (
<GptAssistantPopover <GptAssistantPopover
isOpen={gptAssistantModal} isOpen={gptAssistantModal}
projectId={projectId}
handleClose={() => { handleClose={() => {
setGptAssistantModal((prevData) => !prevData); setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed // this is done so that the title do not reset after gpt popover closed
@ -528,7 +527,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onChange(stateId); onChange(stateId);
handleFormChange(); handleFormChange();
}} }}
projectId={projectId?? undefined} projectId={projectId ?? undefined}
buttonVariant="border-with-text" buttonVariant="border-with-text"
tabIndex={getTabIndex("state_id")} tabIndex={getTabIndex("state_id")}
/> />
@ -651,7 +650,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<div className="h-7"> <div className="h-7">
<ModuleDropdown <ModuleDropdown
projectId={projectId?? undefined} projectId={projectId ?? undefined}
value={value ?? []} value={value ?? []}
onChange={(moduleIds) => { onChange={(moduleIds) => {
onChange(moduleIds); onChange(moduleIds);
@ -748,7 +747,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
handleFormChange(); handleFormChange();
setSelectedParentIssue(issue); setSelectedParentIssue(issue);
}} }}
projectId={projectId?? undefined} projectId={projectId ?? undefined}
issueId={isDraft ? undefined : data?.id} issueId={isDraft ? undefined : data?.id}
/> />
)} )}

View File

@ -21,12 +21,11 @@ type Props = {
editorRef: React.RefObject<EditorRefApi>; editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void; handleDuplicatePage: () => void;
page: IPageStore; page: IPageStore;
projectId: string;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>; readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
}; };
export const PageExtraOptions: React.FC<Props> = observer((props) => { export const PageExtraOptions: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, page, projectId, readOnlyEditorRef } = props; const { editorRef, handleDuplicatePage, page, readOnlyEditorRef } = props;
// states // states
const [gptModalOpen, setGptModal] = useState(false); const [gptModalOpen, setGptModal] = useState(false);
// store hooks // store hooks
@ -56,7 +55,6 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
{isContentEditable && config?.has_openai_configured && ( {isContentEditable && config?.has_openai_configured && (
<GptAssistantPopover <GptAssistantPopover
isOpen={gptModalOpen} isOpen={gptModalOpen}
projectId={projectId}
handleClose={() => { handleClose={() => {
setGptModal((prevData) => !prevData); setGptModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed // this is done so that the title do not reset after gpt popover closed

View File

@ -13,7 +13,6 @@ type Props = {
handleDuplicatePage: () => void; handleDuplicatePage: () => void;
markings: IMarking[]; markings: IMarking[];
page: IPageStore; page: IPageStore;
projectId: string;
sidePeekVisible: boolean; sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void; setSidePeekVisible: (sidePeekState: boolean) => void;
editorReady: boolean; editorReady: boolean;
@ -29,7 +28,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
readOnlyEditorReady, readOnlyEditorReady,
handleDuplicatePage, handleDuplicatePage,
page, page,
projectId,
sidePeekVisible, sidePeekVisible,
setSidePeekVisible, setSidePeekVisible,
} = props; } = props;
@ -56,7 +54,6 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
editorRef={editorRef} editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
page={page} page={page}
projectId={projectId}
readOnlyEditorRef={readOnlyEditorRef} readOnlyEditorRef={readOnlyEditorRef}
/> />
</div> </div>

View File

@ -15,7 +15,6 @@ type Props = {
handleDuplicatePage: () => void; handleDuplicatePage: () => void;
markings: IMarking[]; markings: IMarking[];
page: IPageStore; page: IPageStore;
projectId: string;
sidePeekVisible: boolean; sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void; setSidePeekVisible: (sidePeekState: boolean) => void;
editorReady: boolean; editorReady: boolean;
@ -31,7 +30,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
readOnlyEditorReady, readOnlyEditorReady,
handleDuplicatePage, handleDuplicatePage,
page, page,
projectId,
sidePeekVisible, sidePeekVisible,
setSidePeekVisible, setSidePeekVisible,
} = props; } = props;
@ -66,7 +64,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
editorRef={editorRef} editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
page={page} page={page}
projectId={projectId}
readOnlyEditorRef={readOnlyEditorRef} readOnlyEditorRef={readOnlyEditorRef}
/> />
</div> </div>
@ -79,7 +76,6 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
markings={markings} markings={markings}
handleDuplicatePage={handleDuplicatePage} handleDuplicatePage={handleDuplicatePage}
page={page} page={page}
projectId={projectId}
sidePeekVisible={sidePeekVisible} sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible} setSidePeekVisible={setSidePeekVisible}
/> />

View File

@ -10,8 +10,8 @@ export class AIService extends APIService {
super(API_BASE_URL); super(API_BASE_URL);
} }
async createGptTask(workspaceSlug: string, projectId: string, data: { prompt: string; task: string }): Promise<any> { async createGptTask(workspaceSlug: string, data: { prompt: string; task: string }): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) return this.post(`/api/workspaces/${workspaceSlug}/ai-assistant/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response; throw error?.response;