mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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:
parent
f4ceaaf01c
commit
aa92ace57f
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -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,
|
||||||
|
60
apiserver/plane/app/views/external/base.py
vendored
60
apiserver/plane/app/views/external/base.py
vendored
@ -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(
|
||||||
|
@ -97,7 +97,6 @@ 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}
|
||||||
@ -106,11 +105,9 @@ const PageDetailsPage = observer(() => {
|
|||||||
handleDuplicatePage={handleDuplicatePage}
|
handleDuplicatePage={handleDuplicatePage}
|
||||||
markings={markings}
|
markings={markings}
|
||||||
page={page}
|
page={page}
|
||||||
projectId={projectId.toString()}
|
|
||||||
sidePeekVisible={sidePeekVisible}
|
sidePeekVisible={sidePeekVisible}
|
||||||
setSidePeekVisible={(state) => setSidePeekVisible(state)}
|
setSidePeekVisible={(state) => setSidePeekVisible(state)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<PageEditorBody
|
<PageEditorBody
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
handleEditorReady={(val) => setEditorReady(val)}
|
handleEditorReady={(val) => setEditorReady(val)}
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user