feat: proxy ops ai workspace assistant state
This commit is contained in:
parent
14caa50088
commit
ee4a959a53
|
|
@ -82,6 +82,9 @@ x-app-env: &app-env
|
|||
PLANE_NODEDC_AGENT_GATEWAY_URL: ${PLANE_NODEDC_AGENT_GATEWAY_URL:-}
|
||||
PLANE_NODEDC_AGENT_GATEWAY_TOKEN: ${PLANE_NODEDC_AGENT_GATEWAY_TOKEN:-}
|
||||
PLANE_NODEDC_AGENT_GATEWAY_TIMEOUT_SECONDS: ${PLANE_NODEDC_AGENT_GATEWAY_TIMEOUT_SECONDS:-5}
|
||||
PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL: ${PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL:-}
|
||||
PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN: ${PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN:-}
|
||||
PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TIMEOUT_SECONDS: ${PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TIMEOUT_SECONDS:-8}
|
||||
GUNICORN_WORKERS: 1
|
||||
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
||||
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# See the LICENSE file for details.
|
||||
|
||||
from .analytic import urlpatterns as analytic_urls
|
||||
from .ai_workspace import urlpatterns as ai_workspace_urls
|
||||
from .api import urlpatterns as api_urls
|
||||
from .asset import urlpatterns as asset_urls
|
||||
from .codex_agents import urlpatterns as codex_agent_urls
|
||||
|
|
@ -28,6 +29,7 @@ from .voice_tasker import urlpatterns as voice_tasker_urls
|
|||
|
||||
urlpatterns = [
|
||||
*analytic_urls,
|
||||
*ai_workspace_urls,
|
||||
*asset_urls,
|
||||
*codex_agent_urls,
|
||||
*cycle_urls,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from plane.app.views import (
|
||||
AIWorkspaceExecutorDetailEndpoint,
|
||||
AIWorkspaceExecutorListEndpoint,
|
||||
AIWorkspaceExecutorSelectEndpoint,
|
||||
AIWorkspaceThreadDetailEndpoint,
|
||||
AIWorkspaceThreadListEndpoint,
|
||||
AIWorkspaceThreadMessagesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/ai-workspace/executors/",
|
||||
AIWorkspaceExecutorListEndpoint.as_view(),
|
||||
name="ai-workspace-executors",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/ai-workspace/executors/<uuid:executor_id>/",
|
||||
AIWorkspaceExecutorDetailEndpoint.as_view(),
|
||||
name="ai-workspace-executor-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/ai-workspace/executors/<uuid:executor_id>/select/",
|
||||
AIWorkspaceExecutorSelectEndpoint.as_view(),
|
||||
name="ai-workspace-executor-select",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/ai-workspace/threads/",
|
||||
AIWorkspaceThreadListEndpoint.as_view(),
|
||||
name="ai-workspace-threads",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/ai-workspace/threads/<uuid:thread_id>/",
|
||||
AIWorkspaceThreadDetailEndpoint.as_view(),
|
||||
name="ai-workspace-thread-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/ai-workspace/threads/<uuid:thread_id>/messages/",
|
||||
AIWorkspaceThreadMessagesEndpoint.as_view(),
|
||||
name="ai-workspace-thread-messages",
|
||||
),
|
||||
]
|
||||
|
|
@ -174,6 +174,15 @@ from .module.archive import ModuleArchiveUnarchiveEndpoint
|
|||
|
||||
from .api import ApiTokenEndpoint
|
||||
|
||||
from .ai_workspace import (
|
||||
AIWorkspaceExecutorDetailEndpoint,
|
||||
AIWorkspaceExecutorListEndpoint,
|
||||
AIWorkspaceExecutorSelectEndpoint,
|
||||
AIWorkspaceThreadDetailEndpoint,
|
||||
AIWorkspaceThreadListEndpoint,
|
||||
AIWorkspaceThreadMessagesEndpoint,
|
||||
)
|
||||
|
||||
from .codex_agents import (
|
||||
CodexAgentDetailEndpoint,
|
||||
CodexAgentGrantListEndpoint,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
# Python imports
|
||||
import os
|
||||
|
||||
# Third party imports
|
||||
import requests
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from plane.app.views.base import BaseAPIView
|
||||
|
||||
|
||||
def get_ai_workspace_config():
|
||||
base_url = (
|
||||
os.environ.get("PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL", "").strip()
|
||||
or os.environ.get("NODEDC_AI_WORKSPACE_ASSISTANT_URL", "").strip()
|
||||
or os.environ.get("AI_WORKSPACE_ASSISTANT_URL", "").strip()
|
||||
).rstrip("/")
|
||||
token = (
|
||||
os.environ.get("PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN", "").strip()
|
||||
or os.environ.get("NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN", "").strip()
|
||||
or os.environ.get("AI_WORKSPACE_ASSISTANT_TOKEN", "").strip()
|
||||
or os.environ.get("NODEDC_INTERNAL_ACCESS_TOKEN", "").strip()
|
||||
)
|
||||
timeout = float(os.environ.get("PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TIMEOUT_SECONDS", "8") or "8")
|
||||
return base_url, token, timeout
|
||||
|
||||
|
||||
def require_ai_workspace_config():
|
||||
base_url, token, timeout = get_ai_workspace_config()
|
||||
if not base_url or not token:
|
||||
return None, Response(
|
||||
{
|
||||
"ok": False,
|
||||
"error": "ai_workspace_assistant_not_configured",
|
||||
"message": "NODE.DC AI Workspace Assistant URL/token is not configured.",
|
||||
},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
)
|
||||
return (base_url, token, timeout), None
|
||||
|
||||
|
||||
def user_headers(user):
|
||||
return {
|
||||
"X-NODEDC-User-Id": str(user.id),
|
||||
"X-NODEDC-User-Email": user.email or "",
|
||||
}
|
||||
|
||||
|
||||
def ai_workspace_request(request, method, path, payload=None, query_params=None):
|
||||
config, error_response = require_ai_workspace_config()
|
||||
if error_response is not None:
|
||||
return error_response
|
||||
|
||||
base_url, token, timeout = config
|
||||
try:
|
||||
response = requests.request(
|
||||
method,
|
||||
f"{base_url}/api/ai-workspace/assistant/v1{path}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
**user_headers(request.user),
|
||||
},
|
||||
params=query_params,
|
||||
json=payload,
|
||||
timeout=timeout,
|
||||
)
|
||||
except requests.RequestException:
|
||||
return Response(
|
||||
{
|
||||
"ok": False,
|
||||
"error": "ai_workspace_assistant_unavailable",
|
||||
"message": "NODE.DC AI Workspace Assistant is unavailable.",
|
||||
},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
data = {
|
||||
"ok": False,
|
||||
"error": "ai_workspace_assistant_invalid_response",
|
||||
"message": "NODE.DC AI Workspace Assistant returned a non-JSON response.",
|
||||
}
|
||||
|
||||
return Response(data, status=response.status_code)
|
||||
|
||||
|
||||
def ops_thread_payload(raw_payload, slug):
|
||||
payload = dict(raw_payload or {})
|
||||
active_context = payload.get("activeContext") or payload.get("active_context") or {}
|
||||
if not isinstance(active_context, dict):
|
||||
active_context = {}
|
||||
active_context = {
|
||||
**active_context,
|
||||
"surface": "ops",
|
||||
"workspaceSlug": slug,
|
||||
}
|
||||
|
||||
tool_packs = payload.get("enabledToolPacks") or payload.get("enabled_tool_packs") or []
|
||||
if not isinstance(tool_packs, list):
|
||||
tool_packs = []
|
||||
if "ops" not in tool_packs:
|
||||
tool_packs = [*tool_packs, "ops"]
|
||||
|
||||
payload["originSurface"] = payload.get("originSurface") or payload.get("origin_surface") or "ops"
|
||||
payload["activeContext"] = active_context
|
||||
payload["enabledToolPacks"] = tool_packs
|
||||
return payload
|
||||
|
||||
|
||||
class AIWorkspaceExecutorListEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
return ai_workspace_request(request, "GET", "/executors")
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
return ai_workspace_request(request, "POST", "/executors", request.data)
|
||||
|
||||
|
||||
class AIWorkspaceExecutorDetailEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def patch(self, request, slug, executor_id):
|
||||
return ai_workspace_request(request, "PATCH", f"/executors/{executor_id}", request.data)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def delete(self, request, slug, executor_id):
|
||||
return ai_workspace_request(request, "DELETE", f"/executors/{executor_id}")
|
||||
|
||||
|
||||
class AIWorkspaceExecutorSelectEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def post(self, request, slug, executor_id):
|
||||
return ai_workspace_request(request, "POST", f"/executors/{executor_id}/select")
|
||||
|
||||
|
||||
class AIWorkspaceThreadListEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
query_params = {
|
||||
"surface": request.query_params.get("surface") or "ops",
|
||||
"limit": request.query_params.get("limit") or "100",
|
||||
}
|
||||
return ai_workspace_request(request, "GET", "/threads", query_params=query_params)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
return ai_workspace_request(request, "POST", "/threads", ops_thread_payload(request.data, slug))
|
||||
|
||||
|
||||
class AIWorkspaceThreadDetailEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug, thread_id):
|
||||
return ai_workspace_request(request, "GET", f"/threads/{thread_id}")
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def patch(self, request, slug, thread_id):
|
||||
return ai_workspace_request(request, "PATCH", f"/threads/{thread_id}", ops_thread_payload(request.data, slug))
|
||||
|
||||
|
||||
class AIWorkspaceThreadMessagesEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug, thread_id):
|
||||
query_params = {
|
||||
"limit": request.query_params.get("limit") or "200",
|
||||
}
|
||||
if request.query_params.get("before"):
|
||||
query_params["before"] = request.query_params.get("before")
|
||||
return ai_workspace_request(request, "GET", f"/threads/{thread_id}/messages", query_params=query_params)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def post(self, request, slug, thread_id):
|
||||
return ai_workspace_request(request, "POST", f"/threads/{thread_id}/messages", request.data)
|
||||
Loading…
Reference in New Issue