feat: New Chat, Search Chat and Master Slave DB Architecture for CRM and Oracle Canvas
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user