Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ cp .env.example .env # Configure any environment variables, defined in `./kaize
pre-commit install
```

## Development Tips
- This project is managed by `uv`, not `python` or `pip`, so any python commands need to go through `uv`. All dependencies are defined in `pyproject.toml`.

## Testing Instructions
- Run pytest verbosely with the `-v` flag by default so that you have more context when tests fail.
- Use `uv run pytest tests/.../<test_name.py>` to run tests individually.
- We use the pytest markers `e2e` for end-to-end tests, and `unit` for unit tests, and `phoenix` to test integration with Phoenix.
- When running `uv run pytest` it will skip the tests marked with `phoenix`.
- To run specific markers: `uv run pytest -m e2e` or `uv run pytest -m unit`
- To override and run all: `uv run pytest -m "e2e or unit or phoenix"`
- To run all tests: `uv run pytest -m "e2e or unit or phoenix"`

## Available Interfaces
- MCP Server: `get_guidelines()`, `save_trajectory()`
Expand Down
20 changes: 9 additions & 11 deletions kaizen/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ def __init__(self, config: BaseSettings | None = None):
pass

@abstractmethod
def ready(self):
def ready(self) -> bool:
pass

@abstractmethod
def create_namespace(
self,
namespace_id: str | None = None
) -> Namespace:
def details(self) -> dict:
pass

@abstractmethod
def create_namespace(self, namespace_id: str | None = None) -> Namespace:
pass

@abstractmethod
Expand All @@ -39,15 +40,12 @@ def update_entities(
enable_conflict_resolution: bool = True,
) -> list[EntityUpdate]:
pass

def search_entities(
self,
namespace_id: str,
query: str | None = None,
filters: dict | None = None,
limit: int = 10
self, namespace_id: str, query: str | None = None, filters: dict | None = None, limit: int = 10
) -> list[RecordedEntity]:
pass
Comment on lines 44 to 47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, examine the base.py file to understand the class structure
cat -n kaizen/backend/base.py | head -60

Repository: AgentToolkit/kaizen

Length of output: 1741


🏁 Script executed:

# Search for the class definition and imports to understand if it's already abstract
rg -n "^from abc|^import abc|class BaseEntityBackend|@abstractmethod" kaizen/backend/base.py -B2 -A2

Repository: AgentToolkit/kaizen

Length of output: 1046


🏁 Script executed:

# Find all subclasses of BaseEntityBackend
rg -n "class.*\(.*BaseEntityBackend" kaizen/backend

Repository: AgentToolkit/kaizen

Length of output: 218


🏁 Script executed:

# Check if search_entities is implemented in subclasses
rg -n "def search_entities" kaizen/backend -B3 -A5

Repository: AgentToolkit/kaizen

Length of output: 1704


🏁 Script executed:

# Check pyproject.toml for Ruff configuration
cat pyproject.toml | grep -A20 "\[tool.ruff"

Repository: AgentToolkit/kaizen

Length of output: 109


Add @abstractmethod decorator to search_entities to maintain consistency with other abstract methods in the class.

All other methods in BaseEntityBackend are marked with @abstractmethod, but search_entities is missing this decorator. This inconsistency weakens the interface contract and allows subclasses to skip implementation without raising an error during instantiation.

🛠️ Suggested fix
+    `@abstractmethod`
     def search_entities(
         self, namespace_id: str, query: str | None = None, filters: dict | None = None, limit: int = 10
     ) -> list[RecordedEntity]:
         pass
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def search_entities(
self,
namespace_id: str,
query: str | None = None,
filters: dict | None = None,
limit: int = 10
self, namespace_id: str, query: str | None = None, filters: dict | None = None, limit: int = 10
) -> list[RecordedEntity]:
pass
`@abstractmethod`
def search_entities(
self, namespace_id: str, query: str | None = None, filters: dict | None = None, limit: int = 10
) -> list[RecordedEntity]:
pass
🧰 Tools
🪛 Ruff (0.14.14)

44-47: BaseEntityBackend.search_entities is an empty method in an abstract base class, but has no abstract decorator

(B027)

🤖 Prompt for AI Agents
In `@kaizen/backend/base.py` around lines 44 - 47, The method search_entities in
BaseEntityBackend is missing the `@abstractmethod` decorator; add `@abstractmethod`
immediately above the search_entities definition so subclasses are required to
implement it (and if abstractmethod isn't already imported from abc, add that
import). Ensure the signature of search_entities remains unchanged and matches
other abstract methods in the class.


@abstractmethod
def delete_entity_by_id(self, namespace_id: str, entity_id: str):
pass
pass
28 changes: 11 additions & 17 deletions kaizen/backend/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ def _save_namespace_data(self, namespace_id: str, data: dict):
with open(file_path, "w") as f:
json.dump(data, f, indent=2, default=str)

def ready(self):
def ready(self) -> bool:
"""Check if the backend is healthy."""
return {"status": "ok", "data_dir": str(self.data_dir)}
return True

def details(self) -> dict:
"""Return details about the backend."""
return {"data_dir": str(self.data_dir)}

def create_namespace(self, namespace_id: str | None = None) -> Namespace:
"""Create a new namespace for entities to exist in."""
Expand All @@ -61,9 +65,7 @@ def create_namespace(self, namespace_id: str | None = None) -> Namespace:

with self._lock:
if file_path.exists():
raise NamespaceAlreadyExistsException(
f'Namespace "{namespace_id}" already exists.'
)
raise NamespaceAlreadyExistsException(f'Namespace "{namespace_id}" already exists.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ruff TRY003: avoid long message at raise site.
If TRY003 is enabled, move the message into the exception class or suppress locally.

🛠️ Minimal suppression
-                raise NamespaceAlreadyExistsException(f'Namespace "{namespace_id}" already exists.')
+                raise NamespaceAlreadyExistsException(f'Namespace "{namespace_id}" already exists.')  # noqa: TRY003
As per coding guidelines, `**/*.py`: Use Ruff for linting and formatting in Python files (configured in pyproject.toml).
🧰 Tools
🪛 Ruff (0.14.14)

68-68: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@kaizen/backend/filesystem.py` at line 68, The raise site in filesystem.py
uses a long formatted message in the raise of NamespaceAlreadyExistsException
which triggers Ruff TRY003; either move the message construction into the
exception class (e.g., add an __init__ or classmethod on
NamespaceAlreadyExistsException that builds the message from namespace_id and
then raise NamespaceAlreadyExistsException(namespace_id)) or suppress the linter
at the raise site with a local noqa (TRY003) comment; update the
NamespaceAlreadyExistsException implementation to accept namespace_id and
produce the formatted message so callers (like the raise in filesystem.py)
simply raise the exception with the identifier.


now = datetime.datetime.now(datetime.UTC)
data = {
Expand Down Expand Up @@ -97,9 +99,7 @@ def search_namespaces(self, limit: int = 10) -> list[Namespace]:
namespaces.append(
Namespace(
id=data["id"],
created_at=datetime.datetime.fromisoformat(
data["created_at"]
),
created_at=datetime.datetime.fromisoformat(data["created_at"]),
num_entities=len(data["entities"]),
)
)
Expand Down Expand Up @@ -151,9 +151,7 @@ def update_entities(
# Find similar existing entities for conflict resolution
old_entities = []
for entity in entities:
similar = self._search_entities_internal(
data, query=entity.content, filters=None, limit=10
)
similar = self._search_entities_internal(data, query=entity.content, filters=None, limit=10)
old_entities.extend(similar)

updates = resolve_conflicts(old_entities, entities_with_temporary_ids)
Expand Down Expand Up @@ -181,9 +179,7 @@ def update_entities(
ent["metadata"] = update.metadata
break
case "DELETE":
data["entities"] = [
e for e in data["entities"] if e["id"] != update.id
]
data["entities"] = [e for e in data["entities"] if e["id"] != update.id]
case "NONE":
pass
else:
Expand Down Expand Up @@ -286,9 +282,7 @@ def delete_entity_by_id(self, namespace_id: str, entity_id: str):
with self._lock:
data = self._load_namespace_data(namespace_id)
original_count = len(data["entities"])
data["entities"] = [
e for e in data["entities"] if str(e["id"]) != entity_id
]
data["entities"] = [e for e in data["entities"] if str(e["id"]) != entity_id]
if len(data["entities"]) == original_count:
raise KaizenException(f"Entity `{entity_id}` not found")
self._save_namespace_data(namespace_id, data)
Loading