forked from sagnik/Project_Velocity
feat: Oracle Canvas, Revision History and Canvas Sharing (#33)
Co-authored-by: Sagnik <sagnik7896@gmail.com> Reviewed-on: sagnik/Project_Velocity#33
This commit is contained in:
@@ -56,6 +56,18 @@ def _coerce_datetime(value: datetime | str | None) -> datetime | None:
|
||||
|
||||
# ── Execution store ───────────────────────────────────────────────────────────
|
||||
|
||||
def _json_safe(value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _json_safe(val) for key, val in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_json_safe(item) for item in value]
|
||||
if isinstance(value, tuple):
|
||||
return [_json_safe(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
_DEMO_EXECUTIONS: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
@@ -117,13 +129,13 @@ def _build_demo_retrieval_plan(
|
||||
|
||||
|
||||
_DATASET_MAP: dict[str, str] = {
|
||||
"pipeline_board": "deals",
|
||||
"bar_chart": "lead_daily_snapshot",
|
||||
"pipeline_board": "crm_opportunity_pipeline",
|
||||
"bar_chart": "crm_property_interest_rollup",
|
||||
"geo_map": "lead_geo_interest_rollup",
|
||||
"table": "broker_performance",
|
||||
"line_chart": "inventory_absorption",
|
||||
"table": "crm_contacts_overview",
|
||||
"line_chart": "crm_property_interest_rollup",
|
||||
"kpi_tile": "oracle_aggregated_metric",
|
||||
"activity_stream": "lead_activity_log",
|
||||
"activity_stream": "crm_interaction_timeline",
|
||||
}
|
||||
|
||||
_CODEBOOK_COMPONENT_MAP: dict[str, str] = {
|
||||
@@ -162,6 +174,10 @@ def _dataset_for_codebook(example: CodebookExample, prompt: str, component_plan_
|
||||
return "crm_interaction_timeline"
|
||||
if component_plan_type == "pipeline_board":
|
||||
return "crm_opportunity_pipeline"
|
||||
if component_plan_type == "table" and any(term in lowered_prompt for term in ("last interacted", "last interaction", "recently contacted", "recent interaction")):
|
||||
return "crm_last_interacted_clients"
|
||||
if component_plan_type == "table" and any(term in lowered_prompt for term in ("interest", "interested", "project", "property", "properties")) and any(term in lowered_prompt for term in ("client", "clients", "contact", "contacts")):
|
||||
return "crm_top_interested_clients"
|
||||
if component_plan_type == "line_chart" and any(term in lowered_prompt for term in ("trend", "time", "history", "growth")):
|
||||
return "crm_property_interest_rollup"
|
||||
|
||||
@@ -170,8 +186,12 @@ def _dataset_for_codebook(example: CodebookExample, prompt: str, component_plan_
|
||||
return "crm_interaction_timeline"
|
||||
if "pipeline" in lowered_prompt or "opportunit" in lowered_prompt:
|
||||
return "crm_opportunity_pipeline"
|
||||
if ("interest" in lowered_prompt or "project" in lowered_prompt or "property" in lowered_prompt) and ("client" in lowered_prompt or "contact" in lowered_prompt):
|
||||
return "crm_top_interested_clients"
|
||||
if "interest" in lowered_prompt or "project" in lowered_prompt or "property" in lowered_prompt:
|
||||
return "crm_property_interest_rollup"
|
||||
if "last interacted" in lowered_prompt or "recently contacted" in lowered_prompt or "recent interaction" in lowered_prompt:
|
||||
return "crm_last_interacted_clients"
|
||||
return "crm_contacts_overview"
|
||||
|
||||
if "client" in chapter or "client" in subchapter or "contact" in subchapter:
|
||||
@@ -205,6 +225,7 @@ def _build_codebook_retrieval_plan(
|
||||
exemplar = matches[0]
|
||||
for component_plan_type in desired_types[:4]:
|
||||
dataset = _dataset_for_codebook(exemplar, prompt, component_plan_type)
|
||||
title_hint = _title_for_dataset(dataset, component_plan_type, prompt) or title_hints.get(component_plan_type, exemplar.title)
|
||||
components.append(
|
||||
{
|
||||
"suggestedType": component_plan_type,
|
||||
@@ -222,7 +243,7 @@ def _build_codebook_retrieval_plan(
|
||||
"subchapterName": exemplar.subchapter_name,
|
||||
"sourcePack": exemplar.source_pack,
|
||||
},
|
||||
"titleHint": title_hints.get(component_plan_type, exemplar.title),
|
||||
"titleHint": title_hint,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -235,6 +256,24 @@ def _build_codebook_retrieval_plan(
|
||||
}
|
||||
|
||||
|
||||
def _title_for_dataset(dataset: str, component_plan_type: str, prompt: str) -> str | None:
|
||||
lowered_prompt = prompt.lower()
|
||||
dataset_titles = {
|
||||
"crm_contacts_overview": "CRM Contacts Overview",
|
||||
"crm_opportunity_pipeline": "Opportunity Pipeline",
|
||||
"crm_property_interest_rollup": "Property Interest Rollup",
|
||||
"crm_interaction_timeline": "Client Interaction Timeline",
|
||||
"crm_last_interacted_clients": "Last Interacted Clients",
|
||||
"crm_top_interested_clients": "Top Interested Clients",
|
||||
"broker_performance": "Broker Performance",
|
||||
}
|
||||
if dataset == "crm_top_interested_clients" and "top" in lowered_prompt:
|
||||
return "Top Interested Clients"
|
||||
if dataset == "crm_last_interacted_clients" and ("top" in lowered_prompt or "last" in lowered_prompt):
|
||||
return "Last Interacted Clients"
|
||||
return dataset_titles.get(dataset)
|
||||
|
||||
|
||||
_RUNTIME_ALLOWED_DATASETS = {
|
||||
"deals",
|
||||
"lead_daily_snapshot",
|
||||
@@ -247,6 +286,8 @@ _RUNTIME_ALLOWED_DATASETS = {
|
||||
"crm_opportunity_pipeline",
|
||||
"crm_property_interest_rollup",
|
||||
"crm_interaction_timeline",
|
||||
"crm_last_interacted_clients",
|
||||
"crm_top_interested_clients",
|
||||
}
|
||||
|
||||
|
||||
@@ -371,6 +412,11 @@ class PromptOrchestrator:
|
||||
execution["status"] = "executing"
|
||||
await self._persist_execution(execution)
|
||||
|
||||
page = await canvas_service.get_page(page_id, tenant_id)
|
||||
existing_comps = page.get("components", []) if page else []
|
||||
next_order_base = self._next_order_base(existing_comps)
|
||||
section_id = f"sec_prompt_generated_{execution_id.replace('-', '')[:12]}"
|
||||
|
||||
# ── Step 3: Build visualization plan (component descriptors) ──────────
|
||||
viz_plan = await self._build_visualization_plan(
|
||||
retrieval_plan=retrieval_plan,
|
||||
@@ -382,6 +428,8 @@ class PromptOrchestrator:
|
||||
placement_mode=placement_mode,
|
||||
ctx=ctx,
|
||||
persona_plan=persona_plan,
|
||||
base_order=next_order_base,
|
||||
section_id=section_id,
|
||||
)
|
||||
execution["visualizationPlan"] = viz_plan
|
||||
|
||||
@@ -391,9 +439,7 @@ class PromptOrchestrator:
|
||||
|
||||
# Commit a revision bump with the new components
|
||||
try:
|
||||
page = await canvas_service.get_page(page_id, tenant_id)
|
||||
if page:
|
||||
existing_comps = page.get("components", [])
|
||||
new_comps = existing_comps + viz_plan.get("components", [])
|
||||
revision = await canvas_service.commit_revision(
|
||||
page_id=page_id,
|
||||
@@ -429,6 +475,8 @@ class PromptOrchestrator:
|
||||
placement_mode: str,
|
||||
ctx: PolicyContext,
|
||||
persona_plan: dict[str, Any],
|
||||
base_order: int,
|
||||
section_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Converts a retrieval plan into a list of CanvasComponent descriptors."""
|
||||
components = [
|
||||
@@ -438,9 +486,10 @@ class PromptOrchestrator:
|
||||
branch_id=branch_id,
|
||||
prompt=prompt,
|
||||
persona_plan=persona_plan,
|
||||
order_index=base_order + 100,
|
||||
section_id=section_id,
|
||||
)
|
||||
]
|
||||
base_order = 900 # Append after existing components
|
||||
|
||||
component_plans = retrieval_plan.get("components", [])
|
||||
for i, plan in enumerate(component_plans):
|
||||
@@ -469,7 +518,7 @@ class PromptOrchestrator:
|
||||
"privacyTier": plan.get("privacyTier", "standard"),
|
||||
"cachePolicy": {"mode": "ttl", "ttlSeconds": 120},
|
||||
},
|
||||
"visualizationParameters": self._default_viz_params(ctype, data_rows),
|
||||
"visualizationParameters": self._default_viz_params(ctype, dataset, data_rows),
|
||||
"dataBindings": self._default_bindings(ctype),
|
||||
"version": 1,
|
||||
"lifecycleState": "active",
|
||||
@@ -483,7 +532,7 @@ class PromptOrchestrator:
|
||||
"renderingHints": self._rendering_hints(ctype),
|
||||
"layout": {
|
||||
"orderIndex": base_order + (i + 1) * 100,
|
||||
"sectionId": "sec_prompt_generated",
|
||||
"sectionId": section_id,
|
||||
"widthMode": "full" if ctype in ("pipeline_board", "table", "geo_map") else "half",
|
||||
"minHeightPx": 300,
|
||||
"stickyHeader": False,
|
||||
@@ -520,11 +569,29 @@ class PromptOrchestrator:
|
||||
dataset=dataset,
|
||||
warnings=component_warnings,
|
||||
order_index=base_order + (i + 1) * 100,
|
||||
section_id=section_id,
|
||||
)
|
||||
components.append(comp)
|
||||
|
||||
if len(components) > 1:
|
||||
planning_component = components.pop(0)
|
||||
planning_component["layout"]["orderIndex"] = base_order + (len(component_plans) + 1) * 100
|
||||
components.append(planning_component)
|
||||
|
||||
return {"components": components}
|
||||
|
||||
@staticmethod
|
||||
def _next_order_base(existing_components: list[dict[str, Any]]) -> int:
|
||||
max_existing = 0
|
||||
for component in existing_components:
|
||||
try:
|
||||
order_index = int((component.get("layout") or {}).get("orderIndex", 0))
|
||||
except (TypeError, ValueError):
|
||||
order_index = 0
|
||||
if order_index > max_existing:
|
||||
max_existing = order_index
|
||||
return ((max_existing // 100) + 1) * 100
|
||||
|
||||
@staticmethod
|
||||
def _persona_text_canvas(
|
||||
*,
|
||||
@@ -533,13 +600,13 @@ class PromptOrchestrator:
|
||||
branch_id: str,
|
||||
prompt: str,
|
||||
persona_plan: dict[str, Any],
|
||||
order_index: int,
|
||||
section_id: str,
|
||||
) -> dict[str, Any]:
|
||||
recommended = ", ".join(persona_plan.get("recommendedTemplates", [])) or "no direct template matches"
|
||||
content = (
|
||||
f"Oracle received: {prompt}\n\n"
|
||||
f"Reusable templates: {recommended}\n\n"
|
||||
"Execution policy: query live CRM data first, reuse matching templates, "
|
||||
"synthesize missing UI blocks, then dispatch the required ComfyUI-backed workflow."
|
||||
"Execution policy: query live CRM data first, pick the strongest-fitting canvas components, "
|
||||
"and synthesize any missing UI blocks before rendering the result."
|
||||
)
|
||||
return {
|
||||
"componentId": str(uuid.uuid4()),
|
||||
@@ -574,8 +641,8 @@ class PromptOrchestrator:
|
||||
},
|
||||
"renderingHints": {"estimatedHeightPx": 180, "skeletonVariant": "text", "virtualizationPriority": 4},
|
||||
"layout": {
|
||||
"orderIndex": 910,
|
||||
"sectionId": "sec_prompt_generated",
|
||||
"orderIndex": order_index,
|
||||
"sectionId": section_id,
|
||||
"widthMode": "full",
|
||||
"minHeightPx": 180,
|
||||
"stickyHeader": False,
|
||||
@@ -631,17 +698,34 @@ class PromptOrchestrator:
|
||||
return labels.get(comp_type, "Oracle Canvas Component")
|
||||
|
||||
@staticmethod
|
||||
def _default_viz_params(comp_type: str, rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
def _default_viz_params(comp_type: str, dataset: str, rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
first_row = rows[0] if rows else {}
|
||||
inferred_columns = [key for key in first_row.keys() if key not in {"avatar"}] or ["name", "status"]
|
||||
table_columns_by_dataset: dict[str, list[str]] = {
|
||||
"broker_performance": ["name", "deals_closed", "revenue_generated"],
|
||||
"crm_contacts_overview": ["name", "email", "phone", "city", "buyer_type", "qd_score"],
|
||||
"crm_last_interacted_clients": ["name", "email", "phone", "last_interaction_at", "interaction_count", "qd_score"],
|
||||
"crm_top_interested_clients": ["name", "email", "phone", "interest_count", "projects", "qd_score"],
|
||||
}
|
||||
defaults: dict[str, dict[str, Any]] = {
|
||||
"bar_chart": {"xAxis": "category", "yAxis": "value", "sort": "desc", "showLabels": True, "legend": False},
|
||||
"line_chart": {"showPoints": True, "smooth": True},
|
||||
"kpi_tile": {
|
||||
"label": rows[0].get("metric_label", "Result") if rows else "Result",
|
||||
"trend": str(rows[0].get("trend_value", "")) if rows else "",
|
||||
"comparisonLabel": rows[0].get("comparison_label", "") if rows else "",
|
||||
"label": first_row.get("metric_label", "Result"),
|
||||
"trend": str(first_row.get("trend_value", "")),
|
||||
"comparisonLabel": first_row.get("comparison_label", ""),
|
||||
},
|
||||
"geo_map": {"mapStyle": "dubai_district_heat", "intensityField": "lead_count", "interactive": True, "tooltipFields": ["district", "lead_count", "avg_qd_score"]},
|
||||
"table": {"rankBy": "revenue_generated", "showTopBadge": True, "columns": ["name", "deals_closed", "revenue_generated"]},
|
||||
"table": {
|
||||
"rankBy": "revenue_generated",
|
||||
"showTopBadge": True,
|
||||
"columns": table_columns_by_dataset.get(
|
||||
dataset,
|
||||
inferred_columns,
|
||||
),
|
||||
"emptyStateTitle": "No matching records found",
|
||||
"emptyStateDescription": "The query ran successfully but returned no rows for this prompt.",
|
||||
},
|
||||
"pipeline_board": {"showValue": True, "colorByStage": True},
|
||||
"activity_stream": {"showUrgencyIndicator": True},
|
||||
}
|
||||
@@ -674,7 +758,8 @@ class PromptOrchestrator:
|
||||
def _generate_summary(prompt: str, viz_plan: dict[str, Any]) -> str:
|
||||
count = len(viz_plan.get("components", []))
|
||||
short_prompt = prompt[:60] + ("…" if len(prompt) > 60 else "")
|
||||
return f'Generated {count} component{"s" if count != 1 else ""} for: "{short_prompt}"'
|
||||
data_component_count = max(count - 1, 0)
|
||||
return f'Generated {data_component_count} component{"s" if data_component_count != 1 else ""} for: "{short_prompt}"'
|
||||
|
||||
@staticmethod
|
||||
def _error_component(
|
||||
@@ -686,6 +771,7 @@ class PromptOrchestrator:
|
||||
dataset: str,
|
||||
warnings: list[str],
|
||||
order_index: int,
|
||||
section_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"componentId": component_id,
|
||||
@@ -722,7 +808,7 @@ class PromptOrchestrator:
|
||||
"renderingHints": {"estimatedHeightPx": 140, "skeletonVariant": "generic", "virtualizationPriority": 5},
|
||||
"layout": {
|
||||
"orderIndex": order_index,
|
||||
"sectionId": "sec_prompt_generated",
|
||||
"sectionId": section_id,
|
||||
"widthMode": "full",
|
||||
"minHeightPx": 140,
|
||||
"stickyHeader": False,
|
||||
@@ -875,8 +961,8 @@ class PromptOrchestrator:
|
||||
execution["status"],
|
||||
execution["modelRuntime"],
|
||||
execution["semanticModelVersion"],
|
||||
json.dumps(execution.get("retrievalPlan") or {}),
|
||||
json.dumps(execution.get("visualizationPlan") or {}),
|
||||
json.dumps(_json_safe(execution.get("retrievalPlan") or {})),
|
||||
json.dumps(_json_safe(execution.get("visualizationPlan") or {})),
|
||||
execution.get("warnings", []),
|
||||
execution.get("summary"),
|
||||
execution.get("componentsCreated", []),
|
||||
|
||||
Reference in New Issue
Block a user