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_URL: ${PLANE_NODEDC_AGENT_GATEWAY_URL:-}
|
||||||
PLANE_NODEDC_AGENT_GATEWAY_TOKEN: ${PLANE_NODEDC_AGENT_GATEWAY_TOKEN:-}
|
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_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
|
GUNICORN_WORKERS: 1
|
||||||
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
POSTHOG_API_KEY: ${POSTHOG_API_KEY:-}
|
||||||
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
# See the LICENSE file for details.
|
# See the LICENSE file for details.
|
||||||
|
|
||||||
from .analytic import urlpatterns as analytic_urls
|
from .analytic import urlpatterns as analytic_urls
|
||||||
|
from .ai_workspace import urlpatterns as ai_workspace_urls
|
||||||
from .api import urlpatterns as api_urls
|
from .api import urlpatterns as api_urls
|
||||||
from .asset import urlpatterns as asset_urls
|
from .asset import urlpatterns as asset_urls
|
||||||
from .codex_agents import urlpatterns as codex_agent_urls
|
from .codex_agents import urlpatterns as codex_agent_urls
|
||||||
|
|
@ -28,6 +29,7 @@ from .voice_tasker import urlpatterns as voice_tasker_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
*analytic_urls,
|
*analytic_urls,
|
||||||
|
*ai_workspace_urls,
|
||||||
*asset_urls,
|
*asset_urls,
|
||||||
*codex_agent_urls,
|
*codex_agent_urls,
|
||||||
*cycle_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 .api import ApiTokenEndpoint
|
||||||
|
|
||||||
|
from .ai_workspace import (
|
||||||
|
AIWorkspaceExecutorDetailEndpoint,
|
||||||
|
AIWorkspaceExecutorListEndpoint,
|
||||||
|
AIWorkspaceExecutorSelectEndpoint,
|
||||||
|
AIWorkspaceThreadDetailEndpoint,
|
||||||
|
AIWorkspaceThreadListEndpoint,
|
||||||
|
AIWorkspaceThreadMessagesEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
from .codex_agents import (
|
from .codex_agents import (
|
||||||
CodexAgentDetailEndpoint,
|
CodexAgentDetailEndpoint,
|
||||||
CodexAgentGrantListEndpoint,
|
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