From 0f4171315dae5bc18727157648a00155b3e5bf3d Mon Sep 17 00:00:00 2001 From: Vatche Isahagian Date: Wed, 25 Feb 2026 01:33:20 -0500 Subject: [PATCH 01/24] feat(ui): formalize entity creation with React dropdowns and array mapped triggers --- kaizen/frontend/api/routes.py | 213 ++++++ .../ui/src/components/EntityExplorer.tsx | 611 ++++++++++++++++++ 2 files changed, 824 insertions(+) create mode 100644 kaizen/frontend/api/routes.py create mode 100644 kaizen/frontend/ui/src/components/EntityExplorer.tsx diff --git a/kaizen/frontend/api/routes.py b/kaizen/frontend/api/routes.py new file mode 100644 index 00000000..18667d35 --- /dev/null +++ b/kaizen/frontend/api/routes.py @@ -0,0 +1,213 @@ +from typing import Any, List, Optional +import logging +from pydantic import BaseModel + +from fastapi import APIRouter, Query + +logger = logging.getLogger(__name__) + +router = APIRouter() + +class NamespaceCreateRequest(BaseModel): + namespace_id: str + +class EntityCreateRequest(BaseModel): + type: str + content: str + metadata: dict = {} + +@router.get("/dashboard") +def get_dashboard() -> dict[str, Any]: + from kaizen.frontend.mcp.mcp_server import get_client + client = get_client() + + # 1. Backend health + try: + health = client.ready() + except Exception as e: + logger.error(f"Error checking health: {e}") + health = False + + # 2. Namespace count + try: + namespaces = client.all_namespaces(limit=1000) + namespace_count = len(namespaces) + except Exception as e: + logger.error(f"Error fetching namespaces: {e}") + namespaces = [] + namespace_count = 0 + + # 3. Entity counts and recent entities across namespaces + # For MVP, we will aggregate from all available namespaces up to the limit + total_entities = 0 + type_breakdown: dict[str, int] = {} + recent_entities: list[dict[str, Any]] = [] + + for ns in namespaces: + try: + # We fetch recent entities from this namespace + ns_entities = client.get_all_entities(ns.id, limit=1000) + total_entities += len(ns_entities) + + for entity in ns_entities: + etype = entity.type or "unknown" + type_breakdown[etype] = type_breakdown.get(etype, 0) + 1 + + recent_entities.append({ + "id": entity.id, + "type": entity.type, + "content": entity.content[:100] + "..." if entity.content and len(entity.content) > 100 else entity.content, + "namespace": ns.id, + "created_at": entity.created_at.isoformat() if hasattr(entity, 'created_at') and entity.created_at else None + }) + except Exception as e: + logger.error(f"Error fetching entities for namespace {ns.id}: {e}") + + # sort by created_at descending (assuming we have those or just use the end of list) + # the client doesn't strictly order by date right now unless we extract or sort manually + recent_entities.sort(key=lambda x: x.get("created_at") or "", reverse=True) + recent_entities = recent_entities[:10] # top 10 + + return { + "health": health, + "namespace_count": namespace_count, + "total_entities": total_entities, + "type_breakdown": [{"type": k, "count": v} for k, v in type_breakdown.items()], + "recent_entities": recent_entities + } + +@router.get("/namespaces") +def list_namespaces() -> List[dict[str, Any]]: + from kaizen.frontend.mcp.mcp_server import get_client + client = get_client() + try: + namespaces = [] + if hasattr(client.backend, 'milvus'): + collections = client.backend.milvus.list_collections() + for coll in collections: + try: + count = int(client.backend.milvus.get_collection_stats(coll).get("row_count", 0)) + except Exception: + count = 0 + namespaces.append({"id": coll, "amount_of_entities": count}) + else: + for ns in client.all_namespaces(limit=1000): + namespaces.append({"id": ns.id, "amount_of_entities": ns.num_entities or 0}) + return namespaces + except Exception as e: + logger.error(f"Error fetching namespaces: {e}") + return [] + +@router.post("/namespaces") +def add_namespace(req: NamespaceCreateRequest) -> dict[str, Any]: + from kaizen.frontend.mcp.mcp_server import get_client + client = get_client() + try: + client.create_namespace(req.namespace_id) + return {"success": True, "namespace_id": req.namespace_id} + except Exception as e: + from fastapi import HTTPException + logger.error(f"Error creating namespace: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.delete("/namespaces/{namespace_id}") +def delete_namespace(namespace_id: str) -> dict[str, Any]: + from kaizen.frontend.mcp.mcp_server import get_client + client = get_client() + try: + client.delete_namespace(namespace_id) + return {"success": True} + except Exception as e: + from fastapi import HTTPException + logger.error(f"Error deleting namespace: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/namespaces/{namespace_id}/entities") +def list_namespace_entities( + namespace_id: str, + type: Optional[str] = Query(None, description="Filter entities by type (e.g., guideline, task)"), + limit: int = Query(100, description="Maximum number of entities to return") +) -> List[dict[str, Any]]: + from kaizen.frontend.mcp.mcp_server import get_client + client = get_client() + try: + filters = {} + if type: + filters["type"] = type + + entities = client.get_all_entities(namespace_id, filters=filters, limit=limit) + + result = [] + for entity in entities: + result.append({ + "id": entity.id, + "type": entity.type, + "content": entity.content, + "metadata": entity.metadata or {}, + "created_at": entity.created_at.isoformat() if hasattr(entity, 'created_at') and entity.created_at else None + }) + + # Sort by created_at descending + result.sort(key=lambda x: x.get("created_at") or "", reverse=True) + return result + except Exception as e: + logger.error(f"Error fetching entities for namespace {namespace_id}: {e}") + return [] + +@router.delete("/namespaces/{namespace_id}/entities/{entity_id}") +def delete_namespace_entity(namespace_id: str, entity_id: str) -> dict[str, Any]: + from kaizen.frontend.mcp.mcp_server import get_client + client = get_client() + try: + client.delete_entity_by_id(namespace_id, entity_id) + return {"success": True} + except Exception as e: + from fastapi import HTTPException + logger.error(f"Error deleting entity {entity_id} from namespace {namespace_id}: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/namespaces/{namespace_id}/entities") +def create_namespace_entity(namespace_id: str, req: EntityCreateRequest) -> dict[str, Any]: + from kaizen.frontend.mcp.mcp_server import get_client + from kaizen.schema.core import Entity + from fastapi import HTTPException + + # 1. Enforce specific schema typing prior to insertion + if req.type == "guideline": + from kaizen.schema.tips import Tip + try: + # Tip expects content at the root, so we map req.content and unpack the metadata + Tip(content=req.content, **req.metadata) + except Exception as e: + logger.error(f"Guideline validation failed: {e}") + raise HTTPException(status_code=422, detail=f"Invalid guideline metadata schema: {e}") + + elif req.type == "policy": + try: + from kaizen.schema.policy import Policy + try: + # The Policy model checks the full payload + Policy(content=req.content, type=req.type, **req.metadata) + except Exception as e: + logger.error(f"Policy validation failed: {e}") + raise HTTPException(status_code=422, detail=f"Invalid policy metadata schema: {e}") + except ImportError: + # Fallback if we're on a branch where kaizen.schema.policy doesn't exist yet + logger.warning("Policy schema missing. Skipping strict validation.") + + client = get_client() + try: + new_entity = Entity( + type=req.type, + content=req.content, + metadata=req.metadata + ) + # Using enable_conflict_resolution=False for a direct insert + updates = client.update_entities(namespace_id, [new_entity], enable_conflict_resolution=False) + if not updates: + raise Exception("Failed to insert entity. No updates returned.") + return {"success": True, "id": updates[0].id} + except Exception as e: + from fastapi import HTTPException + logger.error(f"Error creating entity in namespace {namespace_id}: {e}") + raise HTTPException(status_code=400, detail=str(e)) diff --git a/kaizen/frontend/ui/src/components/EntityExplorer.tsx b/kaizen/frontend/ui/src/components/EntityExplorer.tsx new file mode 100644 index 00000000..bf983b43 --- /dev/null +++ b/kaizen/frontend/ui/src/components/EntityExplorer.tsx @@ -0,0 +1,611 @@ +import { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { ArrowLeft, Search, AlertCircle, RefreshCw, Layers, Eye, Trash2, Plus } from 'lucide-react'; + +interface Entity { + id: string; + type: string; + content: string; + created_at?: string; + metadata: Record; +} + +export default function EntityExplorer() { + const { id } = useParams<{ id: string }>(); + const [entities, setEntities] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filterType, setFilterType] = useState(""); + const [selectedEntity, setSelectedEntity] = useState(null); + + // Create Modal State + const [isCreateOpen, setIsCreateOpen] = useState(false); + const [newTypeOption, setNewTypeOption] = useState('guideline'); + const [customType, setCustomType] = useState(''); + const [newContent, setNewContent] = useState(''); + const [newMetadata, setNewMetadata] = useState(''); + const [createError, setCreateError] = useState(null); + + // Guideline Specific State + const [guideRationale, setGuideRationale] = useState(''); + const [guideCategory, setGuideCategory] = useState('strategy'); + const [guideTrigger, setGuideTrigger] = useState(''); + + // Policy Specific State + const [policyName, setPolicyName] = useState(''); + const [policyDesc, setPolicyDesc] = useState(''); + const [policyTypeEnum, setPolicyTypeEnum] = useState('playbook'); + const [policyPriority, setPolicyPriority] = useState(50); + const [policyEnabled, setPolicyEnabled] = useState(true); + + // Policy Trigger Builder State + const [policyTriggers, setPolicyTriggers] = useState([{ type: 'keyword', value: '', target: 'intent', operator: 'or', threshold: 0.7 }]); + + const addTrigger = () => { + setPolicyTriggers([...policyTriggers, { type: 'keyword', value: '', target: 'intent', operator: 'or', threshold: 0.7 }]); + }; + + const removeTrigger = (index: number) => { + setPolicyTriggers(policyTriggers.filter((_, i) => i !== index)); + }; + + const updateTrigger = (index: number, field: string, val: any) => { + const newTriggers = [...policyTriggers]; + newTriggers[index] = { ...newTriggers[index], [field]: val }; + setPolicyTriggers(newTriggers); + }; + + const resetForm = () => { + setNewTypeOption("guideline"); + setCustomType(""); + setNewContent(""); + setNewMetadata(""); + + setGuideRationale(""); + setGuideCategory("strategy"); + setGuideTrigger(""); + + setPolicyName(""); + setPolicyDesc(""); + setPolicyTypeEnum("playbook"); + setPolicyPriority(50); + setPolicyEnabled(true); + setPolicyTriggers([{ type: 'keyword', value: '', target: 'intent', operator: 'or', threshold: 0.7 }]); + + setCreateError(null); + }; + + const fetchEntities = () => { + setLoading(true); + let url = `/api/namespaces/${encodeURIComponent(id || '')}/entities`; + if (filterType) { + url += `?type=${encodeURIComponent(filterType)}`; + } + + fetch(url) + .then(res => { + if (!res.ok) throw new Error('Failed to fetch entities'); + return res.json(); + }) + .then(data => { + setEntities(data); + setError(null); + }) + .catch(err => setError(err.message)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + if (id) { + fetchEntities(); + } + }, [id, filterType]); + + const handleDelete = async (entityId: string) => { + if (!confirm('Are you sure you want to delete this entity?')) return; + + try { + const res = await fetch(`/api/namespaces/${encodeURIComponent(id || '')}/entities/${encodeURIComponent(entityId)}`, { + method: 'DELETE' + }); + if (!res.ok) { + const d = await res.json(); + throw new Error(d.detail || 'Failed to delete entity'); + } + // Refresh list + fetchEntities(); + if (selectedEntity?.id === entityId) { + setSelectedEntity(null); + } + } catch (err: any) { + alert('Error deleting entity: ' + err.message); + } + }; + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + setCreateError(null); + + const activeType = newTypeOption === 'other' ? customType.trim() : newTypeOption; + + if (!activeType) { + setCreateError("Entity type cannot be empty."); + return; + } + + if (!newContent.trim()) { + setCreateError("Entity content cannot be empty."); + return; + } + + let parsedMetadata = {}; + + if (activeType === "guideline") { + if (!guideRationale.trim() || !guideTrigger.trim()) { + setCreateError("Guidelines require a rationale and trigger."); + return; + } + parsedMetadata = { + rationale: guideRationale, + category: guideCategory, + trigger: guideTrigger + }; + } else if (activeType === "policy") { + if (!policyName.trim() || !policyDesc.trim() || policyTriggers.length === 0) { + setCreateError("Policies require a name, description, and at least one trigger."); + return; + } + try { + parsedMetadata = { + name: policyName, + description: policyDesc, + policy_type: policyTypeEnum, + priority: policyPriority, + enabled: policyEnabled, + triggers: policyTriggers + }; + } catch (err) { + setCreateError("Failed to build policy metadata payload."); + return; + } + } else { + if (newMetadata.trim()) { + try { + parsedMetadata = JSON.parse(newMetadata); + } catch (err) { + setCreateError("Metadata must be valid JSON."); + return; + } + } + } + + try { + const res = await fetch(`/api/namespaces/${encodeURIComponent(id || '')}/entities`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: activeType, + content: newContent.trim(), + metadata: parsedMetadata + }) + }); + + if (!res.ok) { + const d = await res.json(); + throw new Error(d.detail || 'Failed to create entity'); + } + + // Reset form and refresh + resetForm(); + setIsCreateOpen(false); + fetchEntities(); + } catch (err: any) { + setCreateError(err.message); + } + }; + + return ( +
+
+
+

+ + + + + Namespace: {id} +

+

+ Browse and filter entities within this namespace +

+
+
+ +
+
+
+ + setFilterType(e.target.value)} + style={{ background: "transparent", border: "none", color: "white", outline: "none", flex: 1 }} + /> +
+
+ + +
+ + {error ? ( +
+ +

{error}

+ +
+ ) : loading ? ( +
+
+
+ ) : ( +
+ + + + + + + + + + + {entities.length === 0 ? ( + + + + ) : ( + entities.map(ent => ( + + + + + + + )) + )} + +
TypeContentCreatedActions
+ No entities found in this namespace. +
+ {ent.type} + +
+ {ent.content.length > 100 ? `${ent.content.substring(0, 100)}...` : ent.content} +
+
+ {ent.created_at ? new Date(ent.created_at).toLocaleString() : 'N/A'} + + + +
+
+ )} + + {selectedEntity && ( +
setSelectedEntity(null)}> +
e.stopPropagation()}> +
+

Entity Details

+ {selectedEntity.type} +
+ +
+ +
+ {selectedEntity.id} +
+
+ +
+ +
+ {selectedEntity.content} +
+
+ + {Object.keys(selectedEntity.metadata).length > 0 && ( +
+ +
+ {JSON.stringify(selectedEntity.metadata, null, 2)} +
+
+ )} + +
+ +
+
+
+ )} + {isCreateOpen && ( +
+
+

Create New Entity

+ + {createError && ( +
+ {createError} +
+ )} + +
+
+ +
+ + {newTypeOption === 'other' && ( + setCustomType(e.target.value)} + placeholder="Enter custom type" + style={{ flex: 1 }} + required + /> + )} +
+
+ +
+ +