feat: Oracle Canvas Component Schema and Qwen 3.6 integration (#31)

Co-authored-by: Sagnik <sagnik7896@gmail.com>
Reviewed-on: #31
This commit was merged in pull request #31.
This commit is contained in:
2026-04-20 01:43:39 +05:30
parent 57144e1bd3
commit e519339cc9
129 changed files with 625213 additions and 262 deletions

View File

@@ -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"]},
]