Oracle Canvas Component Schema and Qwen 3.6 integration
This commit is contained in:
@@ -26,20 +26,23 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Set
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, WebSocket, WebSocketDisconnect, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, get_current_user
|
||||
from .canvas_service import canvas_service
|
||||
from .collaboration_service import collaboration_service
|
||||
from .action_service import oracle_action_service
|
||||
from .persona_service import persona_service
|
||||
from .prompt_orchestrator import prompt_orchestrator
|
||||
from .policy_service import PolicyService, PolicyContext
|
||||
from .codebook_service import codebook_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
policy_svc = PolicyService()
|
||||
_DEFAULT_TENANT_ID = os.getenv("ORACLE_DEFAULT_TENANT_ID", "tenant_velocity")
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -51,13 +54,32 @@ def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _build_user_profile(default_page_id: str) -> dict[str, Any]:
|
||||
def _normalize_oracle_role(role: str) -> str:
|
||||
mapping = {
|
||||
"JUNIOR_BROKER": "junior_broker",
|
||||
"SENIOR_BROKER": "senior_broker",
|
||||
"SALES_DIRECTOR": "sales_director",
|
||||
"ADMIN": "platform_admin",
|
||||
}
|
||||
return mapping.get(role.strip().upper(), "sales_director")
|
||||
|
||||
|
||||
def _build_user_profile(
|
||||
*,
|
||||
user_id: str,
|
||||
email: str,
|
||||
display_name: str,
|
||||
role: str,
|
||||
avatar_url: str | None,
|
||||
default_page_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"userId": os.getenv("ORACLE_DEFAULT_USER_ID", "oracle_operator"),
|
||||
"tenantId": os.getenv("ORACLE_DEFAULT_TENANT_ID", "tenant_velocity"),
|
||||
"email": os.getenv("ORACLE_DEFAULT_EMAIL", "oracle@velocity.local"),
|
||||
"displayName": os.getenv("ORACLE_DEFAULT_DISPLAY_NAME", "Oracle Operator"),
|
||||
"role": os.getenv("ORACLE_DEFAULT_ROLE", "sales_director"),
|
||||
"userId": user_id,
|
||||
"tenantId": _DEFAULT_TENANT_ID,
|
||||
"email": email,
|
||||
"displayName": display_name,
|
||||
"role": _normalize_oracle_role(role),
|
||||
"avatarUrl": avatar_url,
|
||||
"timezone": os.getenv("ORACLE_DEFAULT_TIMEZONE", "Asia/Dubai"),
|
||||
"locale": os.getenv("ORACLE_DEFAULT_LOCALE", "en-AE"),
|
||||
"defaultPageId": default_page_id,
|
||||
@@ -72,17 +94,39 @@ def _build_user_profile(default_page_id: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
async def _get_current_user() -> dict[str, Any]:
|
||||
async def _get_current_user_profile(request: Request, user: UserPrincipal) -> dict[str, Any]:
|
||||
seed_page = await canvas_service.ensure_default_page(
|
||||
tenant_id=os.getenv("ORACLE_DEFAULT_TENANT_ID", "tenant_velocity"),
|
||||
owner_id=os.getenv("ORACLE_DEFAULT_USER_ID", "oracle_operator"),
|
||||
tenant_id=_DEFAULT_TENANT_ID,
|
||||
owner_id=user.user_id,
|
||||
title=os.getenv("ORACLE_DEFAULT_PAGE_TITLE", "Oracle Main Canvas"),
|
||||
)
|
||||
return _build_user_profile(seed_page["pageId"])
|
||||
pool = getattr(request.app.state, "db_pool", None)
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(full_name, split_part(email, '@', 1), id::text) AS display_name,
|
||||
COALESCE(email, id::text || '@velocity.local') AS email,
|
||||
avatar_url
|
||||
FROM users_and_roles
|
||||
WHERE id = $1::uuid
|
||||
""",
|
||||
user.user_id,
|
||||
)
|
||||
return _build_user_profile(
|
||||
user_id=user.user_id,
|
||||
email=row["email"] if row else f"{user.user_id}@velocity.local",
|
||||
display_name=row["display_name"] if row else user.user_id,
|
||||
role=user.role,
|
||||
avatar_url=row["avatar_url"] if row else None,
|
||||
default_page_id=seed_page["pageId"],
|
||||
)
|
||||
|
||||
|
||||
async def _ctx_from_me() -> PolicyContext:
|
||||
me = await _get_current_user()
|
||||
async def _ctx_from_request(request: Request, user: UserPrincipal) -> PolicyContext:
|
||||
me = await _get_current_user_profile(request, user)
|
||||
return PolicyContext(
|
||||
tenant_id=me["tenantId"],
|
||||
actor_id=me["userId"],
|
||||
@@ -143,13 +187,13 @@ class PersonaRenderRequest(BaseModel):
|
||||
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/me", summary="Get current user profile")
|
||||
async def get_me() -> dict:
|
||||
return _ok(await _get_current_user())
|
||||
async def get_me(request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict:
|
||||
return _ok(await _get_current_user_profile(request, user))
|
||||
|
||||
|
||||
@router.get("/canvas-pages/{page_id}", summary="Get canvas page by ID")
|
||||
async def get_canvas_page(page_id: str) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def get_canvas_page(page_id: str, request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
page = await canvas_service.get_page(page_id, ctx.tenant_id)
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail=f"Canvas page '{page_id}' not found.")
|
||||
@@ -157,8 +201,13 @@ async def get_canvas_page(page_id: str) -> dict:
|
||||
|
||||
|
||||
@router.post("/canvas-pages/{page_id}/prompts", summary="Submit a prompt to generate canvas components")
|
||||
async def submit_prompt(page_id: str, payload: PromptSubmitRequest) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def submit_prompt(
|
||||
page_id: str,
|
||||
payload: PromptSubmitRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
execution = await prompt_orchestrator.execute(
|
||||
tenant_id=ctx.tenant_id,
|
||||
page_id=page_id,
|
||||
@@ -198,8 +247,13 @@ async def submit_prompt(page_id: str, payload: PromptSubmitRequest) -> dict:
|
||||
|
||||
|
||||
@router.post("/canvas-pages/{page_id}/forks", summary="Create a fork (share) from a canvas page")
|
||||
async def create_fork(page_id: str, payload: ForkCreateRequest) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def create_fork(
|
||||
page_id: str,
|
||||
payload: ForkCreateRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
page = await canvas_service.get_page(page_id, ctx.tenant_id)
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail="Source page not found.")
|
||||
@@ -214,8 +268,13 @@ async def create_fork(page_id: str, payload: ForkCreateRequest) -> dict:
|
||||
|
||||
|
||||
@router.post("/canvas-pages/{page_id}/rollback", summary="Rollback canvas to a prior revision")
|
||||
async def rollback_canvas(page_id: str, payload: RollbackRequest) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def rollback_canvas(
|
||||
page_id: str,
|
||||
payload: RollbackRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
result = await canvas_service.rollback(
|
||||
page_id=page_id,
|
||||
tenant_id=ctx.tenant_id,
|
||||
@@ -232,38 +291,44 @@ async def rollback_canvas(page_id: str, payload: RollbackRequest) -> dict:
|
||||
|
||||
|
||||
@router.get("/canvas-pages/{page_id}/revisions", summary="List revision history for a canvas page")
|
||||
async def list_revisions(page_id: str) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def list_revisions(page_id: str, request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
revisions = await canvas_service.list_revisions(page_id, ctx.tenant_id)
|
||||
return _ok(revisions, meta={"count": len(revisions)})
|
||||
|
||||
|
||||
@router.get("/component-templates", summary="List component templates")
|
||||
async def list_templates(category: str | None = None, status: str | None = None) -> dict:
|
||||
templates = PREMADE_TEMPLATES
|
||||
if category:
|
||||
templates = [t for t in templates if t["category"] == category]
|
||||
if status:
|
||||
templates = [t for t in templates if t["status"] == status]
|
||||
return _ok(templates, meta={"count": len(templates)})
|
||||
async def list_templates(
|
||||
category: str | None = None,
|
||||
status: str | None = None,
|
||||
search: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> dict:
|
||||
result = codebook_service.list_templates(
|
||||
category=category,
|
||||
status=status,
|
||||
search=search,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return _ok(result["templates"], meta={"count": result["total"], "limit": limit, "offset": offset})
|
||||
|
||||
|
||||
@router.post("/component-templates/synthesize", summary="Synthesize a new component template from a prompt")
|
||||
async def synthesize_template(payload: TemplateSynthesizeRequest) -> dict:
|
||||
me = await _get_current_user()
|
||||
# Stub — full implementation requires Nemoclaw model runtime
|
||||
template = {
|
||||
"templateId": str(uuid.uuid4()),
|
||||
"tenantId": me["tenantId"],
|
||||
"name": "Synthesized Component",
|
||||
"category": "custom",
|
||||
"status": "tenant_draft",
|
||||
"origin": "synthesized",
|
||||
"version": "1.0.0",
|
||||
"acceptedShapes": payload.dataShape,
|
||||
"createdAt": _now(),
|
||||
"updatedAt": _now(),
|
||||
}
|
||||
async def synthesize_template(
|
||||
payload: TemplateSynthesizeRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
me = await _get_current_user_profile(request, user)
|
||||
template = codebook_service.synthesize_template(
|
||||
prompt=payload.prompt,
|
||||
data_shapes=payload.dataShape,
|
||||
)
|
||||
template["tenantId"] = me["tenantId"]
|
||||
template.setdefault("createdAt", _now())
|
||||
template.setdefault("updatedAt", _now())
|
||||
return _ok(template)
|
||||
|
||||
|
||||
@@ -293,8 +358,12 @@ async def list_merge_requests(targetPageId: str | None = None, status: str | Non
|
||||
|
||||
|
||||
@router.post("/merge-requests", summary="Open a merge request")
|
||||
async def create_merge_request(payload: MergeRequestCreateRequest) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def create_merge_request(
|
||||
payload: MergeRequestCreateRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
source_page = await canvas_service.get_page(payload.sourcePageId, ctx.tenant_id)
|
||||
target_page = await canvas_service.get_page(payload.targetPageId, ctx.tenant_id)
|
||||
if not source_page or not target_page:
|
||||
@@ -319,8 +388,13 @@ async def create_merge_request(payload: MergeRequestCreateRequest) -> dict:
|
||||
|
||||
|
||||
@router.post("/merge-requests/{mr_id}/review", summary="Submit a merge request review")
|
||||
async def review_merge_request(mr_id: str, payload: MergeReviewRequest) -> dict:
|
||||
ctx = await _ctx_from_me()
|
||||
async def review_merge_request(
|
||||
mr_id: str,
|
||||
payload: MergeReviewRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ctx = await _ctx_from_request(request, user)
|
||||
mr = await collaboration_service.review_merge_request(
|
||||
mr_id=mr_id,
|
||||
decision=payload.decision,
|
||||
@@ -382,17 +456,3 @@ async def oracle_canvas_ws(ws: WebSocket, page_id: str) -> None:
|
||||
|
||||
# ── Pre-made templates seed ───────────────────────────────────────────────────
|
||||
|
||||
PREMADE_TEMPLATES = [
|
||||
{"templateId": "tpl_kpi_pipeline_health_v1", "tenantId": "_system", "name": "Pipeline Health KPI", "category": "Executive overview", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["scalar", "trend_scalar"]},
|
||||
{"templateId": "tpl_bar_source_quality_v3", "tenantId": "_system", "name": "Lead Source Quality Bar", "category": "Lead quality", "status": "catalog_active", "origin": "premade", "version": "3.0.0", "acceptedShapes": ["categorical_aggregate"]},
|
||||
{"templateId": "tpl_geo_investor_heat_v2", "tenantId": "_system", "name": "Investor Geography Heat Map", "category": "Geographic demand", "status": "catalog_active", "origin": "premade", "version": "2.0.0", "acceptedShapes": ["geospatial_aggregate"]},
|
||||
{"templateId": "tpl_pipeline_board_v2", "tenantId": "_system", "name": "Deal Pipeline Board", "category": "Pipeline management", "status": "catalog_active", "origin": "premade", "version": "2.0.0", "acceptedShapes": ["categorical_records"]},
|
||||
{"templateId": "tpl_broker_performance_v1", "tenantId": "_system", "name": "Broker Performance Ranked", "category": "Broker performance", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["ranked_records"]},
|
||||
{"templateId": "tpl_followup_queue_v1", "tenantId": "_system", "name": "Follow-up Queue", "category": "Operational queues", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["task_records"]},
|
||||
{"templateId": "tpl_investor_timeline_v1", "tenantId": "_system", "name": "Investor Timeline", "category": "Investor timelines", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["chronological_events"]},
|
||||
{"templateId": "tpl_absorption_trend_v1", "tenantId": "_system", "name": "Project Absorption Trend", "category": "Inventory and project analytics", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["time_series"]},
|
||||
{"templateId": "tpl_quota_gauge_v1", "tenantId": "_system", "name": "Quota Attainment Gauge", "category": "Executive overview", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["scalar"]},
|
||||
{"templateId": "tpl_campaign_lead_line_v1", "tenantId": "_system", "name": "Campaign-to-Lead Quality Timeline", "category": "Marketing analytics", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["time_series"]},
|
||||
{"templateId": "tpl_followup_gap_v1", "tenantId": "_system", "name": "Follow-up Gap Report", "category": "Operational queues", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["task_records"]},
|
||||
{"templateId": "tpl_qd_source_compare_v1", "tenantId": "_system", "name": "QD-Weighted Source Comparison", "category": "Lead quality", "status": "catalog_active", "origin": "premade", "version": "1.0.0", "acceptedShapes": ["categorical_aggregate"]},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user