Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0f41713
feat(ui): formalize entity creation with React dropdowns and array ma…
visahak Feb 25, 2026
5036b46
docs: add UI running instructions to README
visahak Feb 25, 2026
370d964
feat(ui): initialize React frontend application
visahak Feb 25, 2026
d1866e0
adding mvp of kaizen UI
visahak Feb 25, 2026
2a0a662
build: include UI dist in package manifest
visahak Feb 25, 2026
4bd8d4d
refactor(ui): fix CSS bugs, decompose EntityExplorer, add shared hook…
visahak Feb 25, 2026
1a5caa8
fix: code quality improvements across backend and frontend
visahak Feb 25, 2026
bcc09de
addressing code review issues
visahak Feb 25, 2026
0762b42
docs: add UI build instructions and run ruff
visahak Feb 25, 2026
2f7e1de
style: apply ruff format to frontend python files
visahak Feb 25, 2026
a1a6700
fixing code review issues
visahak Feb 25, 2026
8a8d62d
more changes to address reviews
visahak Feb 25, 2026
b8c182d
chore: ignore package-lock.json in detect-secrets scan
visahak Feb 25, 2026
5d9f941
address code review issue
visahak Feb 25, 2026
ebb2c8d
addressing more code review issues
visahak Feb 25, 2026
9c3e924
chore: remove kaizen.schema.policy mypy omit and fallback since it no…
visahak Feb 25, 2026
2bf591e
fix(ui): confirm dialog signature, entity creation validation, dashbo…
visahak Mar 13, 2026
5b75be8
fix(ui): provide fallback for dashboard error message
visahak Mar 13, 2026
fb46c6b
fix(api): enforce MAX_ENTITY_LIMIT for entity listing
visahak Mar 13, 2026
dc51fc3
fix(api): raise HTTPException instead of swallowing errors in listing…
visahak Mar 13, 2026
b660ad7
more changes
visahak Mar 13, 2026
3040167
formatting
visahak Mar 13, 2026
8c41ca8
addressing an issue
visahak Mar 13, 2026
51abde4
changes to address issues raised by coderabbit
visahak Mar 25, 2026
7f566d0
Merge branch 'main' into kaizenUI
visahak Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/check-code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,20 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Check typing
run: uv run mypy .

ui-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'kaizen/frontend/ui/package-lock.json'
- name: Install dependencies
working-directory: kaizen/frontend/ui
run: npm install
- name: Run UI Unit Tests
working-directory: kaizen/frontend/ui
run: npm run test -- --run
2 changes: 1 addition & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"exclude": {
"files": "^.secrets.baseline$",
"files": "^.secrets.baseline$|package-lock\\.json$",
"lines": null
},
"generated_at": "2026-02-26T16:00:29Z",
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
recursive-include kaizen *.jinja2
recursive-include kaizen/frontend/ui/dist *
Comment thread
coderabbitai[bot] marked this conversation as resolved.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,40 @@ export OPENAI_API_KEY=sk-...

For LiteLLM proxy usage and model selection (including global fallback via `KAIZEN_MODEL_NAME`), see [CONFIGURATION.md](CONFIGURATION.md).

### Running the MCP Server
### Running the MCP Server & UI

Kaizen provides both a standard MCP server and a full Web UI (Dashboard & Entity Explorer).

> [!IMPORTANT]
> **Building from Source:** If you cloned this repository (rather than installing a pre-built package), you must build the UI before it can be served.
> ```bash
> cd kaizen/frontend/ui
> npm ci && npm run build
> cd ../../../
> ```
> See `kaizen/frontend/ui/README.md` for more frontend development details.

#### Starting Both Automatically
The easiest way to start both the MCP Server (on standard input/output) and the HTTP UI backend is to run the module directly:
```bash
uv run python -m kaizen.frontend.mcp
```
This will start the UI server in the background on port `8000` and the MCP server in the foreground. You can then access the UI locally by opening your browser to:
`http://127.0.0.1:8000/ui/`

#### Starting the UI Standalone
If you only want to access the Web UI and API (without the MCP server stdio blocking the terminal), you can run the FastAPI application directly using `uvicorn`:
```bash
uv run uvicorn kaizen.frontend.mcp.mcp_server:app --host 127.0.0.1 --port 8000
```
Then navigate to `http://127.0.0.1:8000/ui/`.

#### Starting only the MCP Server
If you're attaching Kaizen to an MCP client that requires a direct command (like Claude Desktop):
```bash
uv run fastmcp run kaizen/frontend/mcp/mcp_server.py --transport stdio
```
Or for SSE transport:
```bash
uv run fastmcp run kaizen/frontend/mcp/mcp_server.py --transport sse --port 8201
```
Expand Down
12 changes: 10 additions & 2 deletions kaizen/backend/milvus.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,22 @@ def get_namespace_details(self, namespace_id: str) -> Namespace:
namespace = db_manager.get_namespace(namespace_id)
if namespace is None:
raise NamespaceNotFoundException(f"Namespace {namespace_id} not found")
namespace.num_entities = self.milvus.get_collection_stats(namespace_id)["row_count"]
try:
namespace.num_entities = self.milvus.get_collection_stats(namespace_id)["row_count"]
except Exception as e:
logger.exception(f"Failed to get collection stats for namespace {namespace_id}: {e}")
namespace.num_entities = 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return namespace

def search_namespaces(self, limit: int = 10) -> list[Namespace]:
with SQLiteManager(self.sqlite_uri) as db_manager:
namespaces = []
for namespace in db_manager.search_namespaces(limit):
namespace.num_entities = self.milvus.get_collection_stats(namespace.id)["row_count"]
try:
namespace.num_entities = self.milvus.get_collection_stats(namespace.id)["row_count"]
except Exception as e:
logger.exception(f"Failed to get collection stats for namespace {namespace.id}: {e}")
namespace.num_entities = 0
namespaces.append(namespace)
return namespaces

Expand Down
Empty file added kaizen/frontend/api/__init__.py
Empty file.
244 changes: 244 additions & 0 deletions kaizen/frontend/api/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
from typing import Any, List, Optional
import logging
from pydantic import BaseModel

from fastapi import APIRouter, Query

logger = logging.getLogger(__name__)

router = APIRouter()

MAX_ENTITY_LIMIT = 500


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
approximate_type_breakdown: dict[str, int] = {}
recent_entities: list[dict[str, Any]] = []

for ns in namespaces:
try:
total_entities += ns.num_entities or 0
# Fetch only a small sample per namespace for the dashboard
ns_entities = client.get_all_entities(ns.id, limit=10)

for entity in ns_entities:
etype = entity.type or "unknown"
approximate_type_breakdown[etype] = approximate_type_breakdown.get(etype, 0) + 1

# Safely handle non-string content before slicing
content = entity.content
if isinstance(content, str):
snippet = content[:100] + "..." if len(content) > 100 else content
else:
safe_str = str(content)
snippet = safe_str[:100] + "..." if len(safe_str) > 100 else safe_str

recent_entities.append(
{
"id": entity.id,
"type": entity.type,
"content": snippet,
"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}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
"health": health,
"namespace_count": namespace_count,
"total_entities": total_entities,
"approximate_type_breakdown": [{"type": k, "count": v} for k, v in approximate_type_breakdown.items()],
"type_breakdown_is_approx": True,
"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 = []
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:
from fastapi import HTTPException

logger.error(f"Error fetching namespaces: {e}")
raise HTTPException(status_code=500, detail="Internal server error while fetching namespaces")

Comment thread
coderabbitai[bot] marked this conversation as resolved.

@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="Internal server error while creating namespace")


@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="Internal server error while deleting namespace")


@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=f"Maximum number of entities to return (max {MAX_ENTITY_LIMIT})"),
) -> List[dict[str, Any]]:
from kaizen.frontend.mcp.mcp_server import get_client

client = get_client()
try:
# Sanitize limit
limit = max(1, min(limit, MAX_ENTITY_LIMIT))

filters = {}
if type:
filters["type"] = type

entities = client.get_all_entities(namespace_id, filters=filters, limit=limit)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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: str(x.get("created_at") or ""), reverse=True)
return result
Comment thread
coderabbitai[bot] marked this conversation as resolved.
except Exception as e:
from fastapi import HTTPException

logger.error(f"Error fetching entities for namespace {namespace_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error while fetching entities")


@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="Internal server error while deleting entity")


@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. Normalize and validate inputs before branching
entity_type = req.type.strip().lower()
if not req.content or not req.content.strip():
raise HTTPException(status_code=422, detail="Entity content must be non-empty.")

# 2. Enforce specific schema typing prior to insertion
if entity_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_meta = {k: v for k, v in req.metadata.items() if k != "content"}
Tip(content=req.content, **tip_meta)
except Exception as e:
logger.error(f"Guideline validation failed: {e}")
raise HTTPException(status_code=422, detail=f"Invalid guideline metadata schema: {e}")

elif entity_type == "policy":
from kaizen.schema.policy import Policy, PolicyType

try:
# The Policy model checks the full payload
policy_meta = {k: v for k, v in req.metadata.items() if k != "content"}
Policy(content=req.content, type=PolicyType(req.metadata["policy_type"]), **policy_meta)
except Exception as e:
logger.error(f"Policy validation failed: {e}")
raise HTTPException(status_code=422, detail=f"Invalid policy metadata schema: {e}")

client = get_client()
try:
new_entity = Entity(type=entity_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:
logger.error(f"Error creating entity in namespace {namespace_id}: {e}")
raise HTTPException(status_code=400, detail="Internal server error while creating entity")
18 changes: 17 additions & 1 deletion kaizen/frontend/mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import logging
import sys
import threading
import uvicorn

from kaizen.frontend.mcp.mcp_server import mcp
from kaizen.frontend.mcp.mcp_server import mcp, app

logger = logging.getLogger("kaizen-mcp")


def run_api_server():
"""Run the FastAPI server for UI and API in a background thread."""
try:
# We run with log_level="warning" to avoid cluttering stdio for MCP
uvicorn.run(app, host="127.0.0.1", port=8000, log_level="warning")
except Exception as e:
logging.error(f"Failed to start UI server: {e}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def main():
"""
Main entry point for the server.
"""
# Start the HTTP API/UI server in a daemon thread so it dies when the parent dies
api_thread = threading.Thread(target=run_api_server, daemon=True)
api_thread.start()

try:
# Start FastMCP using stdio (which blocks)
mcp.run()
except KeyboardInterrupt:
logger.info("MCP server stopped by user (KeyboardInterrupt)")
Expand Down
Loading
Loading