feat: New Chat, Search Chat and Master Slave DB Architecture for CRM and Oracle Canvas

This commit is contained in:
Sagnik
2026-04-24 04:18:33 +05:30
parent f04571bd7b
commit eabecf7a25
7 changed files with 1117 additions and 520 deletions

View File

@@ -61,6 +61,8 @@ def _coerce_datetime(value: datetime | str | None) -> datetime | None:
def _json_safe(value: Any) -> Any:
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, uuid.UUID):
return str(value)
if isinstance(value, dict):
return {str(key): _json_safe(val) for key, val in value.items()}
if isinstance(value, list):
@@ -130,6 +132,49 @@ def _build_demo_retrieval_plan(
}
def _infer_chart_axes(rows: list[dict[str, Any]], columns: list[str]) -> tuple[str | None, str | None]:
if not rows or not columns:
return None, None
sample = rows[0]
string_columns = [
column for column in columns
if isinstance(sample.get(column), str) and sample.get(column) not in (None, "")
]
numeric_columns = [
column for column in columns
if isinstance(sample.get(column), (int, float))
]
preferred_dimension_keys = (
"property_name",
"project_name",
"projects",
"name",
"category",
"label",
)
preferred_measure_keys = (
"interested_clients",
"interest_count",
"total_interest_events",
"count",
"value",
"avg_qd_score",
"qd_score",
)
x_axis = next((key for key in preferred_dimension_keys if key in string_columns), None)
if x_axis is None and string_columns:
x_axis = string_columns[0]
y_axis = next((key for key in preferred_measure_keys if key in numeric_columns), None)
if y_axis is None and numeric_columns:
y_axis = numeric_columns[0]
return x_axis, y_axis
_DATASET_MAP: dict[str, str] = {
"pipeline_board": "crm_opportunity_pipeline",
"bar_chart": "oracle_property_interest_rollup",
@@ -168,10 +213,42 @@ def _component_plan_type_from_codebook(example: CodebookExample) -> str:
def _parse_prompt_row_limit(prompt: str, actor_role: str) -> int:
default_limit = 50 if actor_role in ("senior_broker", "junior_broker") else 200
match = re.search(r"\b(?:top|last|latest|recent|first|show|name of the last)\s+(\d{1,4})\b", prompt.lower())
if not match:
lowered = prompt.lower()
match = re.search(r"\b(?:top|last|latest|recent|first|show|name of the last|which)\s+(\d{1,4})\b", lowered)
if match:
requested = max(1, int(match.group(1)))
return min(requested, default_limit)
word_to_number = {
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
"six": 6,
"seven": 7,
"eight": 8,
"nine": 9,
"ten": 10,
"eleven": 11,
"twelve": 12,
"thirteen": 13,
"fourteen": 14,
"fifteen": 15,
"sixteen": 16,
"seventeen": 17,
"eighteen": 18,
"nineteen": 19,
"twenty": 20,
}
word_match = re.search(
r"\b(?:top|last|latest|recent|first|show|name of the last|which)\s+"
r"(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)\b",
lowered,
)
if not word_match:
return default_limit
requested = max(1, int(match.group(1)))
requested = word_to_number[word_match.group(1)]
return min(requested, default_limit)
@@ -416,59 +493,62 @@ class PromptOrchestrator:
next_order_base = self._next_order_base(existing_comps)
section_id = f"sec_prompt_generated_{execution_id.replace('-', '')[:12]}"
natural_result = None
try:
natural_result = await natural_db_agent.execute_prompt(
prompt,
row_limit=_parse_prompt_row_limit(prompt, actor_role),
)
except Exception as exc:
logger.warning("ORCH natural DB agent unavailable, falling back to component planner: %s", exc)
warnings.append(f"Natural DB agent unavailable ({exc}); using component planner fallback.")
if natural_result is not None:
execution["status"] = "executing"
execution["retrievalPlan"] = {
"planId": str(uuid.uuid4()),
"planner": "oracle_natural_db_agent",
"sql": natural_result.sql,
"sourceTables": natural_result.source_tables,
"rowCount": natural_result.row_count,
}
viz_plan = self._build_natural_visualization_plan(
result=natural_result.as_dict(),
prompt=prompt,
execution_id=execution_id,
actor_id=actor_id,
branch_id=branch_id,
base_order=next_order_base,
section_id=section_id,
)
execution["visualizationPlan"] = viz_plan
execution["componentsCreated"] = [c["componentId"] for c in viz_plan.get("components", [])]
try:
if page:
revision = await canvas_service.commit_revision(
page_id=page_id,
tenant_id=tenant_id,
actor_id=actor_id,
commit_kind="prompt",
commit_summary=f"Oracle: {prompt[:80]}",
components=existing_comps + viz_plan.get("components", []),
execution_id=execution_id,
idempotency_key=client_request_id,
)
execution["headRevision"] = revision["revisionNumber"]
except Exception as exc:
logger.warning("ORCH natural revision_commit failed (non-fatal): %s", exc)
warnings.append("Revision commit deferred; will retry on next sync.")
execution["status"] = "completed"
execution["summary"] = self._generate_summary(prompt, viz_plan)
logger.warning("ORCH natural DB agent failed with no fallback enabled: %s", exc)
execution["status"] = "failed"
execution["summary"] = f"Oracle planner failed: {exc}"
execution["completedAt"] = _now()
execution["warnings"] = warnings + natural_result.warnings
execution["warnings"] = warnings + [f"No fallback enabled. Natural planner failure: {exc}"]
await self._persist_execution(execution)
return execution
execution["status"] = "executing"
execution["retrievalPlan"] = {
"planId": str(uuid.uuid4()),
"planner": "oracle_natural_db_agent",
"sql": natural_result.sql,
"sourceTables": natural_result.source_tables,
"rowCount": natural_result.row_count,
}
viz_plan = self._build_natural_visualization_plan(
result=natural_result.as_dict(),
prompt=prompt,
execution_id=execution_id,
actor_id=actor_id,
branch_id=branch_id,
base_order=next_order_base,
section_id=section_id,
)
execution["visualizationPlan"] = viz_plan
execution["componentsCreated"] = [c["componentId"] for c in viz_plan.get("components", [])]
try:
if page:
revision = await canvas_service.commit_revision(
page_id=page_id,
tenant_id=tenant_id,
actor_id=actor_id,
commit_kind="prompt",
commit_summary=f"Oracle: {prompt[:80]}",
components=existing_comps + viz_plan.get("components", []),
execution_id=execution_id,
idempotency_key=client_request_id,
)
execution["headRevision"] = revision["revisionNumber"]
except Exception as exc:
logger.warning("ORCH natural revision_commit failed (non-fatal): %s", exc)
warnings.append("Revision commit deferred; will retry on next sync.")
execution["status"] = "completed"
execution["summary"] = self._generate_summary(prompt, viz_plan)
execution["completedAt"] = _now()
execution["warnings"] = warnings + natural_result.warnings
await self._persist_execution(execution)
return execution
codebook_matches = codebook_service.search_examples(prompt, limit=4)
execution["codebookMatches"] = [
{
@@ -718,6 +798,27 @@ class PromptOrchestrator:
mapped_type = self._map_type(ctype)
dataset = "oracle_natural_sql"
component_id = str(uuid.uuid4())
x_axis, y_axis = _infer_chart_axes(rows, columns)
bindings = self._default_bindings(ctype)
viz_params = {
**self._default_viz_params(ctype, dataset, rows),
"columns": columns,
"sqlSummary": result.get("summary"),
"sourceTables": result.get("sourceTables", []),
"rowCount": result.get("rowCount", len(rows)),
}
if ctype == "bar_chart":
if x_axis:
viz_params["xAxis"] = x_axis
bindings["dimensions"] = [x_axis]
if y_axis:
viz_params["yAxis"] = y_axis
bindings["measures"] = [y_axis]
elif ctype == "line_chart":
if x_axis:
bindings["dimensions"] = [x_axis]
if y_axis:
bindings["measures"] = [y_axis]
comp: dict[str, Any] = {
"componentId": component_id,
"type": mapped_type,
@@ -735,14 +836,8 @@ class PromptOrchestrator:
"privacyTier": "standard",
"cachePolicy": {"mode": "revision_scoped"},
},
"visualizationParameters": {
**self._default_viz_params(ctype, dataset, rows),
"columns": columns,
"sqlSummary": result.get("summary"),
"sourceTables": result.get("sourceTables", []),
"rowCount": result.get("rowCount", len(rows)),
},
"dataBindings": self._default_bindings(ctype),
"visualizationParameters": viz_params,
"dataBindings": bindings,
"version": 1,
"lifecycleState": "active",
"provenance": {
@@ -966,10 +1061,9 @@ class PromptOrchestrator:
@staticmethod
def _generate_summary(prompt: str, viz_plan: dict[str, Any]) -> str:
count = len(viz_plan.get("components", []))
count = len([component for component in viz_plan.get("components", []) if component.get("type") != "textCanvas"])
short_prompt = prompt[:60] + ("" if len(prompt) > 60 else "")
data_component_count = max(count - 1, 0)
return f'Generated {data_component_count} component{"s" if data_component_count != 1 else ""} for: "{short_prompt}"'
return f'Generated {count} component{"s" if count != 1 else ""} for: "{short_prompt}"'
@staticmethod
def _error_component(