forked from sagnik/Project_Velocity
The complete code integration is done. Co-authored-by: Sagnik <sagnik7896@gmail.com> Reviewed-on: sagnik/Project_Velocity#18
347 lines
14 KiB
Python
347 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException
|
|
|
|
try:
|
|
import asyncpg # type: ignore
|
|
except Exception: # pragma: no cover
|
|
asyncpg = None # type: ignore
|
|
|
|
|
|
_DB_URL = os.getenv("DATABASE_URL", "")
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _db_ready() -> bool:
|
|
return bool(_DB_URL and not _DB_URL.startswith("PLACEHOLDER") and asyncpg is not None)
|
|
|
|
|
|
class OracleActionService:
|
|
async def ensure_schema(self) -> None:
|
|
if not _db_ready():
|
|
return
|
|
assert asyncpg is not None
|
|
conn = await asyncpg.connect(_DB_URL)
|
|
try:
|
|
await conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS oracle_actions (
|
|
action_id UUID PRIMARY KEY,
|
|
execution_id UUID,
|
|
tenant_id TEXT NOT NULL,
|
|
page_id UUID,
|
|
branch_id TEXT,
|
|
actor_id TEXT NOT NULL,
|
|
target_entity_type TEXT NOT NULL,
|
|
target_entity_id TEXT,
|
|
action_type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'planned',
|
|
prompt TEXT,
|
|
workflow_dispatch JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
component_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
writeback_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
result_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_oracle_actions_execution ON oracle_actions(execution_id, created_at DESC)"
|
|
)
|
|
await conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_oracle_actions_target ON oracle_actions(target_entity_type, target_entity_id, created_at DESC)"
|
|
)
|
|
finally:
|
|
await conn.close()
|
|
|
|
async def create_from_execution(
|
|
self,
|
|
*,
|
|
execution: dict[str, Any],
|
|
target_entity_type: str = "canvas_page",
|
|
target_entity_id: str | None = None,
|
|
action_type: str = "oracle_canvas_generation",
|
|
writeback_payload: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
action = {
|
|
"actionId": str(uuid.uuid4()),
|
|
"executionId": execution.get("executionId"),
|
|
"tenantId": execution.get("tenantId"),
|
|
"pageId": execution.get("pageId"),
|
|
"branchId": execution.get("branchId"),
|
|
"actorId": execution.get("actorId"),
|
|
"targetEntityType": target_entity_type,
|
|
"targetEntityId": target_entity_id or execution.get("pageId"),
|
|
"actionType": action_type,
|
|
"status": "planned",
|
|
"prompt": execution.get("prompt"),
|
|
"workflowDispatch": execution.get("workflowDispatch") or {},
|
|
"componentIds": execution.get("componentsCreated") or [],
|
|
"writebackPayload": writeback_payload or {},
|
|
"resultPayload": {},
|
|
"createdAt": _now(),
|
|
"updatedAt": _now(),
|
|
}
|
|
await self._persist_action(action)
|
|
return action
|
|
|
|
async def get_action(self, action_id: str) -> dict[str, Any] | None:
|
|
if not _db_ready():
|
|
return None
|
|
assert asyncpg is not None
|
|
conn = await asyncpg.connect(_DB_URL)
|
|
try:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
SELECT action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
|
created_at, updated_at
|
|
FROM oracle_actions
|
|
WHERE action_id = $1::uuid
|
|
""",
|
|
action_id,
|
|
)
|
|
finally:
|
|
await conn.close()
|
|
return self._serialize(row) if row else None
|
|
|
|
async def list_actions(self, *, status: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
|
|
if not _db_ready():
|
|
return []
|
|
assert asyncpg is not None
|
|
conn = await asyncpg.connect(_DB_URL)
|
|
try:
|
|
if status:
|
|
rows = await conn.fetch(
|
|
"""
|
|
SELECT action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
|
created_at, updated_at
|
|
FROM oracle_actions
|
|
WHERE status = $1
|
|
ORDER BY created_at DESC
|
|
LIMIT $2
|
|
""",
|
|
status,
|
|
limit,
|
|
)
|
|
else:
|
|
rows = await conn.fetch(
|
|
"""
|
|
SELECT action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
|
created_at, updated_at
|
|
FROM oracle_actions
|
|
ORDER BY created_at DESC
|
|
LIMIT $1
|
|
""",
|
|
limit,
|
|
)
|
|
finally:
|
|
await conn.close()
|
|
return [self._serialize(row) for row in rows]
|
|
|
|
async def apply_writeback(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
if not _db_ready():
|
|
raise HTTPException(status_code=503, detail="Oracle writeback store unavailable.")
|
|
if payload["target_entity_type"] != "lead":
|
|
raise HTTPException(status_code=422, detail="Only lead writebacks are supported in this pass.")
|
|
|
|
assert asyncpg is not None
|
|
await self.ensure_schema()
|
|
conn = await asyncpg.connect(_DB_URL)
|
|
try:
|
|
target_lead_id = payload["target_entity_id"]
|
|
action_id = payload["action_id"]
|
|
writeback = payload["writeback_payload"]
|
|
|
|
existing = await conn.fetchrow(
|
|
"SELECT id, notes, metadata, kanban_status, qualification, score FROM leads WHERE id = $1",
|
|
target_lead_id,
|
|
)
|
|
if existing is None:
|
|
raise HTTPException(status_code=404, detail=f"Lead '{target_lead_id}' not found for Oracle writeback.")
|
|
|
|
metadata = dict(existing["metadata"] or {})
|
|
metadata_patch = writeback.get("metadata_patch") or {}
|
|
if isinstance(metadata_patch, dict):
|
|
metadata.update(metadata_patch)
|
|
|
|
score = int(existing["score"] or 0) + int(writeback.get("score_delta") or 0)
|
|
updated_notes = (existing["notes"] or "").strip()
|
|
notes_append = writeback.get("notes_append")
|
|
if notes_append:
|
|
separator = "\n\n" if updated_notes else ""
|
|
updated_notes = f"{updated_notes}{separator}{notes_append}"
|
|
|
|
updated = await conn.fetchrow(
|
|
"""
|
|
UPDATE leads
|
|
SET notes = $2,
|
|
metadata = $3::jsonb,
|
|
kanban_status = COALESCE($4, kanban_status),
|
|
qualification = COALESCE($5, qualification),
|
|
score = $6,
|
|
updated_at = NOW()
|
|
WHERE id = $1
|
|
RETURNING id, notes, metadata, kanban_status, qualification, score, updated_at
|
|
""",
|
|
target_lead_id,
|
|
updated_notes,
|
|
json.dumps(metadata),
|
|
writeback.get("kanban_status"),
|
|
writeback.get("qualification"),
|
|
max(score, 0),
|
|
)
|
|
|
|
oracle_message = writeback.get("oracle_message")
|
|
if oracle_message:
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO chat_logs (id, lead_id, sender, channel, content, metadata, created_at)
|
|
VALUES ($1, $2, 'oracle', 'oracle', $3, $4::jsonb, NOW())
|
|
""",
|
|
str(uuid.uuid4()),
|
|
target_lead_id,
|
|
oracle_message,
|
|
json.dumps({"oracle_action_id": action_id, "writeback": True}),
|
|
)
|
|
|
|
result_payload = {
|
|
"lead_id": updated["id"],
|
|
"kanban_status": updated["kanban_status"],
|
|
"qualification": updated["qualification"],
|
|
"score": updated["score"],
|
|
"updated_at": updated["updated_at"].isoformat() if updated["updated_at"] else None,
|
|
}
|
|
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO oracle_actions (
|
|
action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
|
created_at, updated_at
|
|
)
|
|
VALUES (
|
|
$1::uuid, NULL, $2, NULL, NULL, $3,
|
|
$4, $5, $6, 'applied', NULL,
|
|
'{}'::jsonb, '[]'::jsonb, $7::jsonb, $8::jsonb,
|
|
NOW(), NOW()
|
|
)
|
|
ON CONFLICT (action_id)
|
|
DO UPDATE SET
|
|
status = 'applied',
|
|
writeback_payload = EXCLUDED.writeback_payload,
|
|
result_payload = EXCLUDED.result_payload,
|
|
updated_at = NOW()
|
|
""",
|
|
action_id,
|
|
payload.get("tenant_id", "tenant_velocity"),
|
|
payload.get("actor_id", "oracle_operator"),
|
|
payload["target_entity_type"],
|
|
target_lead_id,
|
|
payload.get("action_type", "lead_writeback"),
|
|
json.dumps(writeback),
|
|
json.dumps(result_payload),
|
|
)
|
|
finally:
|
|
await conn.close()
|
|
|
|
return {
|
|
"actionId": action_id,
|
|
"status": "applied",
|
|
"targetEntityType": payload["target_entity_type"],
|
|
"targetEntityId": payload["target_entity_id"],
|
|
"resultPayload": result_payload,
|
|
}
|
|
|
|
async def _persist_action(self, action: dict[str, Any]) -> None:
|
|
if not _db_ready():
|
|
return
|
|
await self.ensure_schema()
|
|
assert asyncpg is not None
|
|
conn = await asyncpg.connect(_DB_URL)
|
|
try:
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO oracle_actions (
|
|
action_id, execution_id, tenant_id, page_id, branch_id, actor_id,
|
|
target_entity_type, target_entity_id, action_type, status, prompt,
|
|
workflow_dispatch, component_ids, writeback_payload, result_payload,
|
|
created_at, updated_at
|
|
)
|
|
VALUES (
|
|
$1::uuid, $2::uuid, $3, $4::uuid, $5, $6,
|
|
$7, $8, $9, $10, $11,
|
|
$12::jsonb, $13::jsonb, $14::jsonb, $15::jsonb,
|
|
$16::timestamptz, $17::timestamptz
|
|
)
|
|
ON CONFLICT (action_id)
|
|
DO UPDATE SET
|
|
status = EXCLUDED.status,
|
|
workflow_dispatch = EXCLUDED.workflow_dispatch,
|
|
component_ids = EXCLUDED.component_ids,
|
|
writeback_payload = EXCLUDED.writeback_payload,
|
|
result_payload = EXCLUDED.result_payload,
|
|
updated_at = EXCLUDED.updated_at
|
|
""",
|
|
action["actionId"],
|
|
action.get("executionId"),
|
|
action["tenantId"],
|
|
action.get("pageId"),
|
|
action.get("branchId"),
|
|
action["actorId"],
|
|
action["targetEntityType"],
|
|
action.get("targetEntityId"),
|
|
action["actionType"],
|
|
action["status"],
|
|
action.get("prompt"),
|
|
json.dumps(action.get("workflowDispatch") or {}),
|
|
json.dumps(action.get("componentIds") or []),
|
|
json.dumps(action.get("writebackPayload") or {}),
|
|
json.dumps(action.get("resultPayload") or {}),
|
|
action["createdAt"],
|
|
action["updatedAt"],
|
|
)
|
|
finally:
|
|
await conn.close()
|
|
|
|
@staticmethod
|
|
def _serialize(row: Any) -> dict[str, Any]:
|
|
return {
|
|
"actionId": str(row["action_id"]),
|
|
"executionId": str(row["execution_id"]) if row["execution_id"] else None,
|
|
"tenantId": row["tenant_id"],
|
|
"pageId": str(row["page_id"]) if row["page_id"] else None,
|
|
"branchId": row["branch_id"],
|
|
"actorId": row["actor_id"],
|
|
"targetEntityType": row["target_entity_type"],
|
|
"targetEntityId": row["target_entity_id"],
|
|
"actionType": row["action_type"],
|
|
"status": row["status"],
|
|
"prompt": row["prompt"],
|
|
"workflowDispatch": row["workflow_dispatch"] or {},
|
|
"componentIds": row["component_ids"] or [],
|
|
"writebackPayload": row["writeback_payload"] or {},
|
|
"resultPayload": row["result_payload"] or {},
|
|
"createdAt": row["created_at"].isoformat() if row["created_at"] else None,
|
|
"updatedAt": row["updated_at"].isoformat() if row["updated_at"] else None,
|
|
}
|
|
|
|
|
|
oracle_action_service = OracleActionService()
|