[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 GPTIntegrationEndpoint
from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpoint
urlpatterns = [
@ -16,4 +16,9 @@ urlpatterns = [
GPTIntegrationEndpoint.as_view(),
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 (
GPTIntegrationEndpoint,
UnsplashEndpoint,
WorkspaceGPTIntegrationEndpoint,
)
from .estimate.base import (
ProjectEstimatePointEndpoint,

View File

@ -11,7 +11,7 @@ from rest_framework import status
# Module imports
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.app.serializers import (
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):
def get(self, request):
(UNSPLASH_ACCESS_KEY,) = get_configuration_value(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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