Skip to content

Commit f8a6202

Browse files
authored
🔨 Update mdc cursor rules and claude.md for backend
2 parents ae952d6 + b0e78c8 commit f8a6202

File tree

8 files changed

+622
-158
lines changed

8 files changed

+622
-158
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
globs: backend/apps/**/*.py
3+
description: App layer (API) contract for FastAPI endpoints in backend/apps. Parse/validate input, call services, map domain errors to HTTP, return JSONResponse on success.
4+
---
5+
6+
### Purpose and Scope
7+
8+
- The App layer is the HTTP boundary for the backend. It applies to files under `backend/apps/*.py`.
9+
- Responsibilities:
10+
- Parse and validate HTTP inputs.
11+
- Call underlying services; do not implement core business logic here.
12+
- Translate domain/service exceptions into `HTTPException` with proper status codes.
13+
- Return `JSONResponse(status_code=HTTPStatus.OK, content=payload)` on success.
14+
- Configuration: Do not access environment variables directly. Read configuration via `consts.const` or pass values through from the request to services.
15+
16+
References: [backend/consts/exceptions.py](mdc:backend/consts/exceptions.py)
17+
18+
### Routing and URL Design
19+
20+
- Keep existing top-level prefixes for compatibility (e.g., `"/agent"`, `"/memory"`). When adding new modules or endpoints, follow these rules:
21+
- Use plural nouns for collection-style resources (e.g., `"/agents"`, `"/memories"`).
22+
- Use snake_case for all path segments. Avoid hyphens and camelCase.
23+
- Prefer resource-oriented paths for CRUD-style operations. Example: `"/agents"` (collection), `"/agents/{agent_id}"` (single resource).
24+
- Use action-style paths only when necessary to match current patterns or when the operation is not naturally CRUD (e.g., `"/agent/run"`, `"/agent/stop/{conversation_id}"`).
25+
- Path parameters must be singular, semantic nouns: `"/agents/{agent_id}"`, `"/memories/{memory_id}"`.
26+
- Keep backwards compatibility: do not rename existing routes; new routes should follow these conventions.
27+
28+
### HTTP Methods
29+
30+
- GET: Read and list operations only. Maintain existing special cases where GET performs safe actions (e.g., `GET /agent/stop/{conversation_id}`), but do not introduce new side-effecting GETs.
31+
- POST: Create resources, perform searches, or trigger actions with side effects (e.g., `POST /memory/add`, `POST /memory/search`, `POST /agent/run`).
32+
- DELETE: Delete resources or clear collections (e.g., `DELETE /memory/clear`). Ensure idempotency.
33+
- PUT/PATCH: Update resources. Prefer `PUT` for full updates and `PATCH` for partial updates. Preserve legacy `POST /update` endpoints for compatibility but favor PUT/PATCH for new code.
34+
35+
### Authorization and Identity
36+
37+
- Retrieve the bearer token via header injection: `authorization: Optional[str] = Header(None)`.
38+
- Use utility helpers to parse identity (prefer functions in `utils.auth_utils`, such as `get_current_user_id` or `get_current_user_info`) and pass `user_id` and/or `tenant_id` down to services. The App layer should not implement token parsing logic itself.
39+
40+
### Request Validation
41+
42+
- Prefer Pydantic models in `consts.model` as request bodies for complex payloads (e.g., `AgentRequest`).
43+
- For simple atomic fields, use `Body(..., embed=True)` to pin the JSON key name.
44+
- Use `Query(...)` for filters and pagination, `Path(...)` for path parameters, and `Header(...)` for headers.
45+
- Pagination recommendations for listing endpoints: `page: int = Query(1, ge=1)`, `page_size: int = Query(20, ge=1, le=100)`, plus optional `order_by`, `filters` as appropriate. Return pagination metadata (`items`, `total`) or match existing return shapes in the codebase.
46+
47+
### Responses
48+
49+
- On success, return `JSONResponse(status_code=HTTPStatus.OK, content=payload)`.
50+
- If a standard response model exists in the project (e.g., conversation responses), continue to use it for consistency.
51+
- For new endpoints, return a structured content dictionary with necessary fields (e.g., `{"data": ..., "message": "OK"}`) while staying consistent with existing patterns.
52+
53+
### Exception Mapping
54+
55+
- Catch domain/service exceptions from `backend/consts/exceptions.py` and map to `HTTPException` with appropriate status codes. Examples:
56+
- `UnauthorizedError` → 401 UNAUTHORIZED
57+
- `LimitExceededError` → 429 TOO_MANY_REQUESTS
58+
- Parameter/validation errors (e.g., invalid enum, unknown config key) → 400 BAD_REQUEST or 406 NOT_ACCEPTABLE (follow existing precedent such as `set_single_config` using 406)
59+
- Unexpected errors → 500 INTERNAL_SERVER_ERROR (log the error; do not leak internal details)
60+
61+
### Logging and Observability
62+
63+
- Use a module-level logger: `logger = logging.getLogger("<module_name>")`.
64+
- Log key events and errors. For listing/search endpoints, optionally log query scope and timing while avoiding sensitive data.
65+
66+
### Async/Sync Conventions
67+
68+
- Match the existing style in each module. Keep `async def` where already used.
69+
- When calling async services, prefer direct `await`. When calling sync services, invoke them directly without creating new event loops.
70+
71+
### Backward Compatibility
72+
73+
- Do not break existing routes, payload shapes, or response structures.
74+
- New endpoints should follow these conventions strictly to converge the API style across modules.
75+
76+
### Correct Example (parse input, call service, map exceptions, return JSONResponse)
77+
```python
78+
from http import HTTPStatus
79+
import logging
80+
from fastapi import APIRouter, HTTPException
81+
from starlette.responses import JSONResponse
82+
83+
from consts.exceptions import LimitExceededError, AgentRunException, MemoryPreparationException
84+
from services.agent_service import run_agent
85+
86+
logger = logging.getLogger(__name__)
87+
router = APIRouter()
88+
89+
@router.post("/agent/run")
90+
def run_agent_endpoint(payload: dict):
91+
try:
92+
result = run_agent(payload)
93+
return JSONResponse(status_code=HTTPStatus.OK, content=result)
94+
except LimitExceededError as exc:
95+
raise HTTPException(status_code=HTTPStatus.TOO_MANY_REQUESTS, detail=str(exc))
96+
except MemoryPreparationException as exc:
97+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(exc))
98+
except AgentRunException as exc:
99+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc))
100+
```
101+
102+
### Incorrect Example (business logic in App layer or non-HTTP error handling)
103+
```python
104+
from starlette.responses import JSONResponse
105+
106+
def run_agent_endpoint(payload: dict):
107+
# WRONG: performing core business logic inside the app layer
108+
if payload.get("force"):
109+
return {"status": "forced"} # WRONG: returns plain dict without HTTP status context
110+
111+
# WRONG: not translating domain errors to HTTP
112+
result = risky_logic(payload)
113+
return JSONResponse(result)
114+
```
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
globs: backend/database/**/*.py
3+
description: Database layer standards for models, CRUD, transactions, and exceptions
4+
---
5+
6+
# Database Layer Standards
7+
8+
Scope: all Python under `backend/database/**/*.py`. Concise standards for models, CRUD, transactions, and exceptions.
9+
10+
- Models: define in [backend/database/db_models.py](mdc:backend/database/db_models.py).
11+
- Sessions: use `get_db_session()` from [backend/database/client.py](mdc:backend/database/client.py).
12+
- Exceptions: share a DB exception type in [backend/consts/exceptions.py](mdc:backend/consts/exceptions.py).
13+
- SQLAlchemy Core: prefer `insert`/`update`/`select` with `session.execute()`/`session.scalars()`; ORM `session.add()` is allowed but not default.
14+
15+
## 1) Models and audit fields
16+
- Inherit all models from `TableBase`.
17+
- Shared fields: `create_time`, `update_time`, `created_by`, `updated_by`, `delete_flag` (`Y`/`N`).
18+
- Never re-declare shared fields; add only table-specific columns.
19+
20+
## 2) CRUD and audit
21+
- Create: set `created_by`, `updated_by`, default `delete_flag='N'`; timestamps are server-managed.
22+
- Update: set `updated_by`; do not change `create_time`/`created_by`.
23+
- Delete: soft-delete only (`delete_flag='Y'`, set `updated_by`). Cascade by soft-deleting children in same transaction when needed.
24+
- Read: exclude soft-deleted rows by default (`delete_flag='N'`).
25+
26+
## 3) Transactions and sessions
27+
- Always use `with get_db_session() as session:`.
28+
- Never call `commit()`, `rollback()`, or `close()` in DB-layer code.
29+
- The context manager centrally handles commit/rollback/close.
30+
31+
## 4) Exceptions
32+
- Do not catch DB exceptions in `backend/database/**`; let them propagate.
33+
- Central handling occurs in `get_db_session()`.
34+
- Services that must proceed non-blockingly may catch a shared type (e.g., `DatabaseOperationError`).
35+
36+
## 5) Exception flow (inside get_db_session)
37+
- On exception: `rollback` → re-raise → `close` → propagate to callers.
38+
39+
## 6) Reference patterns (Core; no explicit commit/rollback)
40+
```python
41+
from sqlalchemy import insert, update, select
42+
from database.client import get_db_session, as_dict
43+
44+
def create_entity(data: dict):
45+
with get_db_session() as session:
46+
return session.execute(
47+
insert(SomeModel).values(**data).returning(SomeModel.id)
48+
).scalar_one()
49+
50+
def update_entity(entity_id: int, updates: dict, actor: str):
51+
with get_db_session() as session:
52+
session.execute(
53+
update(SomeModel)
54+
.where(SomeModel.id == entity_id, SomeModel.delete_flag == 'N')
55+
.values(**updates, updated_by=actor)
56+
)
57+
58+
def soft_delete_entity(entity_id: int, actor: str):
59+
with get_db_session() as session:
60+
session.execute(
61+
update(SomeModel)
62+
.where(SomeModel.id == entity_id, SomeModel.delete_flag == 'N')
63+
.values(delete_flag='Y', updated_by=actor)
64+
)
65+
66+
def read_active_entity(entity_id: int):
67+
with get_db_session() as session:
68+
record = session.scalars(
69+
select(SomeModel).where(
70+
SomeModel.id == entity_id,
71+
SomeModel.delete_flag == 'N',
72+
)
73+
).first()
74+
return None if record is None else as_dict(record)
75+
```
76+
77+
## 7) Validation checklist
78+
- All models inherit `TableBase`; no duplicated audit fields.
79+
- Deletes are soft deletes (`delete_flag='Y'`) and set `updated_by`.
80+
- No direct `commit`/`rollback`/`close` outside `get_db_session()`.
81+
- No DB exception catching in `backend/database/` modules.
82+
- Reads default to `delete_flag='N'`.
83+
- Services that must proceed on failure catch a shared DB exception type in `consts.exceptions`.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
globs: backend/services/**/*.py
3+
description: Service layer implements core business logic orchestration; raise custom exceptions; no HTTP handling
4+
---
5+
6+
### Service Layer Rules
7+
8+
- **Scope**: Applies to `backend/services/*.py`.
9+
- **Goal**: Implement core business logic and orchestrate complex workflows. Coordinate repositories/SDKs. Keep HTTP concerns out of this layer.
10+
- **Exceptions**: Raise domain/service exceptions declared in `backend/consts/exceptions.py`. If a new case is needed, add a new class there, then raise it here. Do not translate to HTTP here.
11+
- **Environment variables**: Do not access `os.getenv()` directly. Read configuration from `consts.const` (see `environment_variable` rule) or accept parameters.
12+
13+
Reference: [backend/consts/exceptions.py](mdc:backend/consts/exceptions.py)
14+
15+
### Correct example (service orchestrates business logic and raises domain exceptions)
16+
```python
17+
# backend/services/agent_service.py
18+
from typing import Any, Dict
19+
20+
from consts.exceptions import LimitExceededError, AgentRunException, MemoryPreparationException
21+
# from consts.const import APPID, TOKEN # Example: read config via consts, not os.getenv
22+
23+
24+
def run_agent(task_payload: Dict[str, Any]) -> Dict[str, Any]:
25+
"""Run agent core workflow and return domain result dict.
26+
Raises domain exceptions on failure; no HTTP concerns here.
27+
"""
28+
if _is_rate_limited(task_payload):
29+
raise LimitExceededError("Too many requests for this tenant.")
30+
31+
try:
32+
memory = _prepare_memory(task_payload)
33+
except Exception as exc:
34+
# Wrap low-level error in a domain exception for the app layer to translate
35+
raise MemoryPreparationException("Failed to prepare memory.") from exc
36+
37+
try:
38+
result = _execute_core_logic(task_payload, memory)
39+
except Exception as exc:
40+
raise AgentRunException("Agent execution failed.") from exc
41+
42+
# Return a plain Python object, not a Response
43+
return {"status": "ok", "data": result}
44+
45+
46+
def _is_rate_limited(_: Dict[str, Any]) -> bool:
47+
return False
48+
49+
50+
def _prepare_memory(_: Dict[str, Any]) -> Dict[str, Any]:
51+
return {"memo": "prepared"}
52+
53+
54+
def _execute_core_logic(_: Dict[str, Any], __: Dict[str, Any]) -> Dict[str, Any]:
55+
return {"answer": "42"}
56+
```
57+
58+
### Incorrect example (service leaks HTTP/web concerns or reads env directly)
59+
```python
60+
# backend/services/agent_service.py
61+
from fastapi import HTTPException # WRONG: HTTP in service
62+
from starlette.responses import JSONResponse # WRONG: Response in service
63+
import os # WRONG: direct env access in service
64+
65+
66+
def run_agent(_: dict):
67+
# WRONG: translating to HTTP inside service
68+
if os.getenv("RATE_LIMIT", "0") == "1": # WRONG: direct getenv here
69+
raise HTTPException(status_code=429, detail="Too many requests")
70+
71+
# WRONG: returning framework response from service
72+
return JSONResponse({"status": "ok"})
73+
```
74+
75+
### Declaring a new custom exception (do this in exceptions module)
76+
```python
77+
# backend/consts/exceptions.py
78+
class OrderProcessingError(Exception):
79+
"""Raised when order processing fails in service layer."""
80+
pass
81+
```
82+
83+
### Existing exceptions (excerpt from current code)
84+
```python
85+
"""
86+
Custom exception classes for the application.
87+
"""
88+
89+
90+
class AgentRunException(Exception):
91+
"""Exception raised when agent run fails."""
92+
pass
93+
94+
95+
class LimitExceededError(Exception):
96+
"""Raised when an outer platform calling too frequently"""
97+
pass
98+
99+
100+
class UnauthorizedError(Exception):
101+
"""Raised when a user from outer platform is unauthorized."""
102+
pass
103+
104+
105+
class SignatureValidationError(Exception):
106+
"""Raised when X-Signature header is missing or does not match the expected HMAC value."""
107+
pass
108+
109+
110+
class MemoryPreparationException(Exception):
111+
"""Raised when memory preprocessing or retrieval fails prior to agent run."""
112+
pass
113+
```

0 commit comments

Comments
 (0)