elric-cli 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elric_cli-1.0.0/.github/workflows/publish-pypi.yml +36 -0
- elric_cli-1.0.0/.gitignore +13 -0
- elric_cli-1.0.0/.python-version +1 -0
- elric_cli-1.0.0/.windsurf/rules/00-elric-boost.md +97 -0
- elric_cli-1.0.0/.windsurf/rules/01-python-fastapi.md +119 -0
- elric_cli-1.0.0/.windsurf/rules/02-agents-langchain.md +121 -0
- elric_cli-1.0.0/.windsurf/rules/03-database-sqlmodel.md +85 -0
- elric_cli-1.0.0/.windsurf/rules/04-docker-uv.md +112 -0
- elric_cli-1.0.0/.windsurf/rules/05-elric-conventions.md +118 -0
- elric_cli-1.0.0/LICENSE +21 -0
- elric_cli-1.0.0/PKG-INFO +98 -0
- elric_cli-1.0.0/README.md +69 -0
- elric_cli-1.0.0/elric_cli/__init__.py +1 -0
- elric_cli-1.0.0/elric_cli/app.py +24 -0
- elric_cli-1.0.0/elric_cli/commands/.gitkeep +0 -0
- elric_cli-1.0.0/elric_cli/commands/__init__.py +0 -0
- elric_cli-1.0.0/elric_cli/commands/apikey.py +134 -0
- elric_cli-1.0.0/elric_cli/commands/make.py +266 -0
- elric_cli-1.0.0/elric_cli/commands/migrate.py +69 -0
- elric_cli-1.0.0/elric_cli/commands/project.py +72 -0
- elric_cli-1.0.0/elric_cli/commands/route.py +49 -0
- elric_cli-1.0.0/elric_cli/commands/serve.py +33 -0
- elric_cli-1.0.0/elric_cli/install.sh +92 -0
- elric_cli-1.0.0/elric_cli/stubs/.gitkeep +0 -0
- elric_cli-1.0.0/elric_cli/stubs/agent.stub.py +23 -0
- elric_cli-1.0.0/elric_cli/stubs/agent_chat.stub.py +62 -0
- elric_cli-1.0.0/elric_cli/stubs/agent_planner.stub.py +82 -0
- elric_cli-1.0.0/elric_cli/stubs/agent_react.stub.py +83 -0
- elric_cli-1.0.0/elric_cli/stubs/agent_simple.stub.py +32 -0
- elric_cli-1.0.0/elric_cli/stubs/agent_streaming.stub.py +59 -0
- elric_cli-1.0.0/elric_cli/stubs/agent_tool.stub.py +63 -0
- elric_cli-1.0.0/elric_cli/stubs/chain.stub.py +23 -0
- elric_cli-1.0.0/elric_cli/stubs/controller.stub.py +30 -0
- elric_cli-1.0.0/elric_cli/stubs/event.stub.py +18 -0
- elric_cli-1.0.0/elric_cli/stubs/exception.stub.py +21 -0
- elric_cli-1.0.0/elric_cli/stubs/job.stub.py +24 -0
- elric_cli-1.0.0/elric_cli/stubs/listener.stub.py +23 -0
- elric_cli-1.0.0/elric_cli/stubs/middleware.stub.py +32 -0
- elric_cli-1.0.0/elric_cli/stubs/migration.stub.py +26 -0
- elric_cli-1.0.0/elric_cli/stubs/model.stub.py +18 -0
- elric_cli-1.0.0/elric_cli/stubs/route.stub.py +33 -0
- elric_cli-1.0.0/elric_cli/stubs/schema.stub.py +32 -0
- elric_cli-1.0.0/elric_cli/stubs/test.stub.py +21 -0
- elric_cli-1.0.0/elric_cli/stubs/tool.stub.py +24 -0
- elric_cli-1.0.0/elric_cli/uninstall.sh +64 -0
- elric_cli-1.0.0/elric_cli/utils.py +181 -0
- elric_cli-1.0.0/pyproject.toml +62 -0
- elric_cli-1.0.0/uv.lock +293 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install build tooling
|
|
24
|
+
run: python -m pip install --upgrade pip build twine
|
|
25
|
+
|
|
26
|
+
- name: Build package
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Check package metadata
|
|
30
|
+
run: python -m twine check dist/*
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
env:
|
|
34
|
+
TWINE_USERNAME: __token__
|
|
35
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
36
|
+
run: python -m twine upload dist/*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Elric Boost Guidelines
|
|
6
|
+
|
|
7
|
+
The Elric Boost guidelines are specifically curated for this application. Follow them closely to ensure the best experience when building Elric applications.
|
|
8
|
+
|
|
9
|
+
## Foundational Context
|
|
10
|
+
|
|
11
|
+
This application is an Elric framework application. You are an expert with all of these packages and versions. Ensure you abide by them.
|
|
12
|
+
|
|
13
|
+
- python - 3.12
|
|
14
|
+
- fastapi - >=0.135.1
|
|
15
|
+
- uvicorn - >=0.42.0
|
|
16
|
+
- sqlmodel - >=0.0.37
|
|
17
|
+
- alembic - >=1.13
|
|
18
|
+
- asyncpg - >=0.31.0
|
|
19
|
+
- redis[asyncio] - >=5.0
|
|
20
|
+
- langchain - >=1.2.12
|
|
21
|
+
- langgraph - >=1.1.0
|
|
22
|
+
- langsmith - >=0.7.20
|
|
23
|
+
- pydantic-settings - >=2.0
|
|
24
|
+
- structlog - >=24.0
|
|
25
|
+
- typer - >=0.12
|
|
26
|
+
- jinja2 - >=3.1
|
|
27
|
+
- pytest - >=8.0
|
|
28
|
+
- pytest-asyncio - >=0.26
|
|
29
|
+
- ruff - >=0.15.6
|
|
30
|
+
|
|
31
|
+
## Skills Activation
|
|
32
|
+
|
|
33
|
+
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain — don't wait until you're stuck.
|
|
34
|
+
|
|
35
|
+
- `agent-development` — Activates whenever creating or editing files in `app/agents/`, `app/chains/`, or `app/tools/`. Use when building LangGraph graphs, LangChain chains, defining tool functions, or wiring LangSmith tracing.
|
|
36
|
+
- `route-development` — Activates whenever creating or editing files in `app/routes/` or `app/controllers/`. Use when defining FastAPI routers, dependency injection, request schemas, or response models.
|
|
37
|
+
- `database-development` — Activates whenever creating or editing files in `database/models/` or `database/migrations/`. Use when defining SQLModel entities, writing Alembic migrations, or querying via async session.
|
|
38
|
+
|
|
39
|
+
## Conventions
|
|
40
|
+
|
|
41
|
+
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
|
42
|
+
- Use descriptive names for variables and functions. For example, `is_rate_limited`, `has_valid_api_key`, not `check()`.
|
|
43
|
+
- Use `snake_case` for all files, folders, variables, and functions. Use `PascalCase` only for class names.
|
|
44
|
+
- Check for existing base classes and utilities to reuse before writing new ones.
|
|
45
|
+
|
|
46
|
+
## CLI — Always Use the Elric CLI
|
|
47
|
+
|
|
48
|
+
- Use `uv run elric make:` commands to create new files (agents, chains, tools, routes, controllers, schemas, models, migrations, jobs, exceptions, tests).
|
|
49
|
+
- Never create these files manually — always go through the CLI to ensure stubs are applied correctly.
|
|
50
|
+
- Use `uv run elric route:list` to inspect registered routes before adding new ones.
|
|
51
|
+
- Use `uv run elric migrate:status` to check migration state before writing a new migration.
|
|
52
|
+
|
|
53
|
+
## Verification
|
|
54
|
+
|
|
55
|
+
- Do not create ad-hoc scripts to verify behavior when tests cover that functionality. Pytest tests are more important.
|
|
56
|
+
- Use `uv run pytest tests/ -x -q` to run the test suite.
|
|
57
|
+
|
|
58
|
+
## Application Structure & Architecture
|
|
59
|
+
|
|
60
|
+
- Stick to the existing directory structure. Do not create new top-level folders without approval.
|
|
61
|
+
- Do not change dependencies in `pyproject.toml` without approval.
|
|
62
|
+
- All configuration must go through `config/settings.py` (Pydantic Settings). Never use `os.environ` directly in application code.
|
|
63
|
+
|
|
64
|
+
## Replies
|
|
65
|
+
|
|
66
|
+
- Be concise — focus on what matters, not obvious details.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Elric Boost Tools
|
|
71
|
+
|
|
72
|
+
### CLI Commands
|
|
73
|
+
|
|
74
|
+
- Before running a `uv run elric` command, verify the available subcommands with `uv run elric --help` or `uv run elric make --help` if unsure.
|
|
75
|
+
|
|
76
|
+
### Database
|
|
77
|
+
|
|
78
|
+
- Use `uv run elric migrate:status` to check current migration state.
|
|
79
|
+
- Use `uv run elric db:seed` to seed the database with initial data.
|
|
80
|
+
- Never use raw SQL. Always use SQLModel + async session.
|
|
81
|
+
|
|
82
|
+
### Debugging
|
|
83
|
+
|
|
84
|
+
- Use `uv run pytest --tb=short -x` to debug failing tests quickly.
|
|
85
|
+
- Inspect structlog JSON output to trace requests end-to-end via `trace_id`.
|
|
86
|
+
- Use the LangSmith trace UI to debug agent and chain runs when `LANGCHAIN_TRACING_V2=true`.
|
|
87
|
+
|
|
88
|
+
### Reading Logs
|
|
89
|
+
|
|
90
|
+
- All logs are structured JSON. Filter by `trace_id` to follow a single request through the full stack.
|
|
91
|
+
- In development, set `LOG_JSON=false` for human-readable output.
|
|
92
|
+
|
|
93
|
+
### Searching Documentation
|
|
94
|
+
|
|
95
|
+
- Always search official documentation before making implementation decisions for FastAPI, LangGraph, LangChain, LangSmith, SQLModel, or Alembic.
|
|
96
|
+
- Use multiple simple topic-based queries. For example: `['langgraph state', 'langgraph nodes edges']`.
|
|
97
|
+
- Do not include package names in queries — search by concept.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: glob
|
|
3
|
+
globs: ["app/**/*.py", "elric_cli/**/*.py", "config/**/*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python & FastAPI
|
|
7
|
+
|
|
8
|
+
## Foundational Rules
|
|
9
|
+
|
|
10
|
+
- Always use `async def` for all I/O-bound operations (database, Redis, HTTP calls, file I/O).
|
|
11
|
+
- Use `def` only for pure CPU-bound functions with no I/O.
|
|
12
|
+
- Always use explicit return type annotations on every function and method.
|
|
13
|
+
- Always use type hints for all parameters. Prefer Pydantic models over raw `dict` for input/output.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# CORRECT
|
|
17
|
+
async def get_api_key(key_id: uuid.UUID, session: AsyncSession) -> ApiKeyResponse:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
# WRONG
|
|
21
|
+
async def get_api_key(key_id, session):
|
|
22
|
+
...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## FastAPI Conventions
|
|
26
|
+
|
|
27
|
+
- Use `APIRouter` for each domain — never register routes directly on the `app` instance.
|
|
28
|
+
- Always use `Depends()` for dependency injection (DB session, API key, settings).
|
|
29
|
+
- Use `HTTPException` for expected, well-defined errors in controllers.
|
|
30
|
+
- For unexpected errors, let them propagate to the `GlobalExceptionHandler` — never use bare `except Exception`.
|
|
31
|
+
- Use the `lifespan` context manager for startup/shutdown. Never use `@app.on_event`.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# CORRECT — lifespan
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def lifespan(app: FastAPI):
|
|
37
|
+
await init_db()
|
|
38
|
+
await init_redis()
|
|
39
|
+
yield
|
|
40
|
+
|
|
41
|
+
# WRONG
|
|
42
|
+
@app.on_event("startup")
|
|
43
|
+
async def startup():
|
|
44
|
+
...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Request & Response Schemas
|
|
48
|
+
|
|
49
|
+
- Every route must have an explicit Pydantic schema for request body and response.
|
|
50
|
+
- Always generate schemas with: `uv run elric make:schema SchemeName`
|
|
51
|
+
- Keep request schemas separate from response schemas — never reuse the same model for both.
|
|
52
|
+
- Use the RORO pattern (Receive an Object, Return an Object) for all controllers.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# CORRECT — separate schemas
|
|
56
|
+
class CreateChatRequest(BaseModel):
|
|
57
|
+
message: str
|
|
58
|
+
session_id: uuid.UUID
|
|
59
|
+
|
|
60
|
+
class ChatResponse(BaseModel):
|
|
61
|
+
reply: str
|
|
62
|
+
trace_id: str
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Error Handling
|
|
66
|
+
|
|
67
|
+
- Never handle exceptions in controllers. Raise `ElricException` or one of its subclasses.
|
|
68
|
+
- The `GlobalExceptionHandler` in `app/exceptions/handler.py` handles everything centrally.
|
|
69
|
+
- To create new exception types: `uv run elric make:exception ExceptionName`
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# CORRECT
|
|
73
|
+
async def run_agent(request: RunAgentRequest) -> AgentResponse:
|
|
74
|
+
result = await agent.run(request.input)
|
|
75
|
+
if not result:
|
|
76
|
+
raise NotFoundException("Agent produced no output")
|
|
77
|
+
return AgentResponse(**result)
|
|
78
|
+
|
|
79
|
+
# WRONG
|
|
80
|
+
async def run_agent(request: RunAgentRequest) -> AgentResponse:
|
|
81
|
+
try:
|
|
82
|
+
result = await agent.run(request.input)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return {"error": str(e)}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
- Never use `os.environ` directly in application code.
|
|
90
|
+
- Always use `get_settings()` from `config/settings.py`.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# CORRECT
|
|
94
|
+
from config.settings import get_settings
|
|
95
|
+
settings = get_settings()
|
|
96
|
+
redis_url = settings.REDIS_URL
|
|
97
|
+
|
|
98
|
+
# WRONG
|
|
99
|
+
import os
|
|
100
|
+
redis_url = os.environ.get("REDIS_URL")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Imports
|
|
104
|
+
|
|
105
|
+
- Required order: stdlib → third-party → internal (`app/`, `config/`, `database/`).
|
|
106
|
+
- Use absolute imports only. Never relative imports (`from ..models import ...`).
|
|
107
|
+
|
|
108
|
+
## Naming
|
|
109
|
+
|
|
110
|
+
- Files and folders: `snake_case` (e.g. `chat_agent.py`, `api_key.py`)
|
|
111
|
+
- Classes: `PascalCase` (e.g. `ChatAgent`, `ApiKeyMiddleware`)
|
|
112
|
+
- Functions and variables: `snake_case` with auxiliary verbs (e.g. `is_active`, `has_expired`, `get_session`)
|
|
113
|
+
- Constants: `UPPER_SNAKE_CASE`
|
|
114
|
+
|
|
115
|
+
## Content-Type Checking (FastAPI >=0.135)
|
|
116
|
+
|
|
117
|
+
- FastAPI now enforces strict Content-Type validation by default for JSON endpoints.
|
|
118
|
+
- If a client does not send `Content-Type: application/json`, the request will be rejected with 422.
|
|
119
|
+
- To disable this for a specific route (e.g. for legacy clients): `app = FastAPI(strict_content_type=False)`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: glob
|
|
3
|
+
globs: ["app/agents/**/*.py", "app/chains/**/*.py", "app/tools/**/*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agents, Chains & Tools — LangGraph / LangChain / LangSmith
|
|
7
|
+
|
|
8
|
+
- IMPORTANT: Activate the `agent-development` skill whenever working in `app/agents/`, `app/chains/`, or `app/tools/`.
|
|
9
|
+
- CRITICAL: Always search official LangGraph and LangChain documentation before implementing new patterns — APIs evolve quickly across major versions.
|
|
10
|
+
|
|
11
|
+
## Package Versions in Use
|
|
12
|
+
|
|
13
|
+
- `langgraph` >=1.1.0 — stable 1.x API, breaking changes from 0.x
|
|
14
|
+
- `langchain` >=1.2.12 — stable 1.x API, breaking changes from 0.x
|
|
15
|
+
- `langsmith` >=0.7.20
|
|
16
|
+
|
|
17
|
+
## Agents — LangGraph
|
|
18
|
+
|
|
19
|
+
- Every agent must extend `BaseAgent` from `app/agents/base_agent.py`. Never instantiate `StateGraph` directly in a controller.
|
|
20
|
+
- Always generate the file with: `uv run elric make:agent AgentName`
|
|
21
|
+
- The `build_graph()` method must return an uncompiled `StateGraph`. Compilation happens inside `run()`.
|
|
22
|
+
- Always use `ainvoke()` — never `invoke()` in an async context.
|
|
23
|
+
- Always define state as a `TypedDict` or dataclass — never use a raw `dict` as state.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# CORRECT — typed state
|
|
27
|
+
from typing import TypedDict
|
|
28
|
+
|
|
29
|
+
class ChatState(TypedDict):
|
|
30
|
+
messages: list[str]
|
|
31
|
+
context: str
|
|
32
|
+
output: str | None
|
|
33
|
+
|
|
34
|
+
class ChatAgent(BaseAgent):
|
|
35
|
+
name = "chat_agent"
|
|
36
|
+
|
|
37
|
+
def build_graph(self) -> StateGraph:
|
|
38
|
+
graph = StateGraph(ChatState)
|
|
39
|
+
graph.add_node("process", self._process)
|
|
40
|
+
graph.add_node("respond", self._respond)
|
|
41
|
+
graph.add_edge("process", "respond")
|
|
42
|
+
graph.set_entry_point("process")
|
|
43
|
+
return graph
|
|
44
|
+
|
|
45
|
+
async def _process(self, state: ChatState) -> ChatState:
|
|
46
|
+
...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Chains — LangChain
|
|
50
|
+
|
|
51
|
+
- Every chain must extend `BaseChain` from `app/chains/base_chain.py`.
|
|
52
|
+
- Always generate with: `uv run elric make:chain ChainName`
|
|
53
|
+
- Use `ainvoke()` — never `invoke()` or the deprecated `run()`.
|
|
54
|
+
- Always define `PromptTemplate` as a class attribute, not inline inside a method.
|
|
55
|
+
- Always use an explicit `output_parser` — never do manual string parsing of the output.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# CORRECT
|
|
59
|
+
class SummarizeChain(BaseChain):
|
|
60
|
+
name = "summarize_chain"
|
|
61
|
+
|
|
62
|
+
prompt = PromptTemplate.from_template(
|
|
63
|
+
"Summarize the following text in {language}:\n\n{text}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def run(self, text: str, language: str = "English") -> str:
|
|
67
|
+
chain = self.prompt | self.llm | StrOutputParser()
|
|
68
|
+
return await chain.ainvoke({"text": text, "language": language})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Tools — LangChain
|
|
72
|
+
|
|
73
|
+
- Always generate with: `uv run elric make:tool ToolName`
|
|
74
|
+
- Use the `@tool` decorator from LangChain or extend `BaseTool`.
|
|
75
|
+
- Every tool must have a docstring — LangChain uses it as the description passed to the LLM.
|
|
76
|
+
- Use `args_schema` with a Pydantic model to validate tool inputs.
|
|
77
|
+
- Tools must be pure functions — no undeclared side effects.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# CORRECT
|
|
81
|
+
class SearchInput(BaseModel):
|
|
82
|
+
query: str
|
|
83
|
+
max_results: int = 5
|
|
84
|
+
|
|
85
|
+
@tool(args_schema=SearchInput)
|
|
86
|
+
async def search_documents(query: str, max_results: int = 5) -> list[dict]:
|
|
87
|
+
"""Search internal documents by semantic similarity."""
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## LangSmith Tracing
|
|
92
|
+
|
|
93
|
+
- Tracing is enabled automatically by `app/providers/langsmith.py` when `LANGCHAIN_TRACING_V2=true`.
|
|
94
|
+
- Do not add manual tracing in controllers — `BaseAgent` and `BaseChain` handle it.
|
|
95
|
+
- To attach custom metadata to a run, use `langsmith.trace()` as a context manager.
|
|
96
|
+
- In development, use the LangSmith UI to inspect input/output for every graph node.
|
|
97
|
+
- In production, every run is automatically tagged with the `trace_id` from the HTTP request.
|
|
98
|
+
|
|
99
|
+
## LangGraph Node Naming
|
|
100
|
+
|
|
101
|
+
- Use descriptive `snake_case` names for all nodes: `validate_input`, `fetch_context`, `generate_response`.
|
|
102
|
+
- Never use generic names like `step1`, `process`, `node`.
|
|
103
|
+
- Conditional edge routers (`add_conditional_edges`) must use a named router function.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# CORRECT
|
|
107
|
+
graph.add_conditional_edges(
|
|
108
|
+
"validate_input",
|
|
109
|
+
self._route_by_intent, # explicit named router function
|
|
110
|
+
{"chat": "generate_response", "search": "fetch_context"}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# WRONG
|
|
114
|
+
graph.add_conditional_edges("process", lambda x: x["type"], {...})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## LangGraph 1.x Notes
|
|
118
|
+
|
|
119
|
+
- `StateGraph` now supports `interrupt_before` and `interrupt_after` for human-in-the-loop — prefer these over manual checkpointing.
|
|
120
|
+
- Use `MemorySaver` for in-process state persistence during development; replace with a persistent checkpointer in production.
|
|
121
|
+
- `CompiledGraph.stream()` and `CompiledGraph.astream()` are the preferred way to stream node outputs to clients.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: glob
|
|
3
|
+
globs: ["database/**/*.py", "app/providers/database.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database — SQLModel, PostgreSQL, Alembic
|
|
7
|
+
|
|
8
|
+
- IMPORTANT: Activate the `database-development` skill whenever working in `database/` or `app/providers/database.py`.
|
|
9
|
+
|
|
10
|
+
## SQLModel Models
|
|
11
|
+
|
|
12
|
+
- Always generate with: `uv run elric make:model ModelName`
|
|
13
|
+
- Every model has a UUID `id` as primary key, generated automatically.
|
|
14
|
+
- Every model has `created_at` and `updated_at` managed automatically.
|
|
15
|
+
- One file per entity in `database/models/`. Never define multiple models in the same file.
|
|
16
|
+
- Use `Field()` for all fields with constraints (index, unique, foreign key, default).
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# CORRECT — standard model structure
|
|
20
|
+
class Post(SQLModel, table=True):
|
|
21
|
+
__tablename__ = "posts"
|
|
22
|
+
|
|
23
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
24
|
+
title: str = Field(index=True)
|
|
25
|
+
content: str
|
|
26
|
+
is_published: bool = Field(default=False)
|
|
27
|
+
author_id: uuid.UUID = Field(foreign_key="users.id")
|
|
28
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
29
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
30
|
+
|
|
31
|
+
# Relationships
|
|
32
|
+
author: "User" = Relationship(back_populates="posts")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Queries — Async Session
|
|
36
|
+
|
|
37
|
+
- Always use `AsyncSession` — never the synchronous session.
|
|
38
|
+
- Inject the session via `Depends(get_session)` in controllers.
|
|
39
|
+
- Prefer SQLModel's `select()` over raw queries.
|
|
40
|
+
- Prevent N+1 queries with `selectinload()` or `joinedload()` for relationships.
|
|
41
|
+
- Never use raw DB connections — always go through the ORM.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# CORRECT — async query with eager loading
|
|
45
|
+
async def get_posts_with_authors(session: AsyncSession) -> list[Post]:
|
|
46
|
+
statement = (
|
|
47
|
+
select(Post)
|
|
48
|
+
.where(Post.is_published == True)
|
|
49
|
+
.options(selectinload(Post.author))
|
|
50
|
+
.order_by(Post.created_at.desc())
|
|
51
|
+
)
|
|
52
|
+
result = await session.exec(statement)
|
|
53
|
+
return result.all()
|
|
54
|
+
|
|
55
|
+
# WRONG — N+1
|
|
56
|
+
posts = await session.exec(select(Post)).all()
|
|
57
|
+
for post in posts:
|
|
58
|
+
print(post.author.name) # N separate queries!
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Migrations — Alembic
|
|
62
|
+
|
|
63
|
+
- Always generate with: `uv run elric make:migration <description_in_snake_case>`
|
|
64
|
+
- Use clear descriptions: `add_is_published_to_posts`, `create_api_keys_table`.
|
|
65
|
+
- Before every migration: run `uv run elric migrate:status` to verify current state.
|
|
66
|
+
- When modifying a column, include **all** previously defined attributes — Alembic does not preserve them automatically.
|
|
67
|
+
- Never modify migration files that have already been run in production. Always create a new migration.
|
|
68
|
+
- Every migration must have both `upgrade()` and `downgrade()` fully implemented.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# CORRECT — downgrade always implemented
|
|
72
|
+
def upgrade() -> None:
|
|
73
|
+
op.add_column("posts", sa.Column("is_published", sa.Boolean(), nullable=False, server_default="false"))
|
|
74
|
+
|
|
75
|
+
def downgrade() -> None:
|
|
76
|
+
op.drop_column("posts", "is_published")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Seeders
|
|
80
|
+
|
|
81
|
+
- Generate with: `uv run elric make:seeder SeederName`
|
|
82
|
+
- Run with: `uv run elric db:seed`
|
|
83
|
+
- Seeders must be idempotent — use `INSERT ... ON CONFLICT DO NOTHING` or a get-or-create pattern.
|
|
84
|
+
- Use seeders for mandatory initial data (e.g. first admin API key), not for test data.
|
|
85
|
+
- For tests, use pytest fixtures defined in `tests/conftest.py`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: glob
|
|
3
|
+
globs: ["docker/**", "Dockerfile*", "docker-compose*.yml", "pyproject.toml", ".env*"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docker, uv & Environments
|
|
7
|
+
|
|
8
|
+
## uv — Package Manager
|
|
9
|
+
|
|
10
|
+
- Always use `uv` to manage dependencies. Never run `pip install` directly.
|
|
11
|
+
- To add a dependency: `uv add <package>`
|
|
12
|
+
- To add a dev dependency: `uv add --dev <package>`
|
|
13
|
+
- To run commands in the virtualenv: `uv run <command>`
|
|
14
|
+
- The `uv.lock` file must always be committed — it guarantees reproducible builds.
|
|
15
|
+
- Never edit `pyproject.toml` manually for dependencies — use `uv add` / `uv remove`.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# CORRECT
|
|
19
|
+
uv add httpx
|
|
20
|
+
uv run pytest tests/
|
|
21
|
+
|
|
22
|
+
# WRONG
|
|
23
|
+
pip install httpx
|
|
24
|
+
python -m pytest tests/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Environment Variables
|
|
28
|
+
|
|
29
|
+
- `.env` is gitignored — never commit it.
|
|
30
|
+
- `.env.example` is the canonical reference — update it every time you add a new variable.
|
|
31
|
+
- All variables go through `config/settings.py` (Pydantic Settings). Never use `os.environ` in application code.
|
|
32
|
+
- Different values per environment: `.env` for local dev → Docker env vars for production.
|
|
33
|
+
- In production, variables are injected via `docker-compose.prod.yml` or the cloud provider's secrets manager.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# CORRECT — always via Settings
|
|
37
|
+
from config.settings import get_settings
|
|
38
|
+
settings = get_settings()
|
|
39
|
+
db_url = settings.DATABASE_URL
|
|
40
|
+
|
|
41
|
+
# WRONG
|
|
42
|
+
import os
|
|
43
|
+
db_url = os.environ["DATABASE_URL"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Dockerfile — Development
|
|
47
|
+
|
|
48
|
+
- The dev image mounts the code as a volume for hot-reload.
|
|
49
|
+
- Uvicorn runs with `--reload` in development.
|
|
50
|
+
- Do not install production-only dependencies in the dev image.
|
|
51
|
+
|
|
52
|
+
```dockerfile
|
|
53
|
+
FROM python:3.12-slim
|
|
54
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
55
|
+
WORKDIR /app
|
|
56
|
+
COPY pyproject.toml uv.lock ./
|
|
57
|
+
RUN uv sync --frozen
|
|
58
|
+
COPY . .
|
|
59
|
+
CMD ["uv", "run", "uvicorn", "app:create_app", "--factory", "--reload", "--host", "0.0.0.0"]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Dockerfile — Production (multi-stage)
|
|
63
|
+
|
|
64
|
+
- Always use a multi-stage build: `builder` stage + `production` stage.
|
|
65
|
+
- The `builder` stage installs dependencies with `uv sync --frozen --no-dev`.
|
|
66
|
+
- The `production` stage copies only `.venv` from the builder — minimal final image.
|
|
67
|
+
- The app must never run as `root` in production. Always use a non-privileged user.
|
|
68
|
+
- Use Gunicorn with `UvicornWorker` in production.
|
|
69
|
+
- Note: `uvicorn.workers` module is deprecated in uvicorn >=0.42. Use the separate `uvicorn-worker` package instead.
|
|
70
|
+
|
|
71
|
+
```dockerfile
|
|
72
|
+
# Builder stage
|
|
73
|
+
FROM python:3.12-slim AS builder
|
|
74
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
75
|
+
WORKDIR /app
|
|
76
|
+
COPY pyproject.toml uv.lock ./
|
|
77
|
+
RUN uv sync --frozen --no-dev
|
|
78
|
+
|
|
79
|
+
# Production stage
|
|
80
|
+
FROM python:3.12-slim AS production
|
|
81
|
+
WORKDIR /app
|
|
82
|
+
COPY --from=builder /app/.venv ./.venv
|
|
83
|
+
COPY . .
|
|
84
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
85
|
+
RUN adduser --disabled-password --no-create-home appuser
|
|
86
|
+
USER appuser
|
|
87
|
+
# Requires: uv add uvicorn-worker gunicorn
|
|
88
|
+
CMD ["gunicorn", "app:create_app", "--factory", \
|
|
89
|
+
"--worker-class", "uvicorn_worker.UvicornWorker", \
|
|
90
|
+
"--workers", "4", "--bind", "0.0.0.0:8000"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Healthcheck
|
|
94
|
+
|
|
95
|
+
- Every service in `docker-compose.yml` must have a `healthcheck` configured.
|
|
96
|
+
- The app depends on `db` and `redis` with `condition: service_healthy`.
|
|
97
|
+
- The app's `/health` endpoint is used as the healthcheck by the load balancer.
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
# CORRECT
|
|
101
|
+
healthcheck:
|
|
102
|
+
test: ["CMD-SHELL", "pg_isready -U elric"]
|
|
103
|
+
interval: 5s
|
|
104
|
+
timeout: 3s
|
|
105
|
+
retries: 5
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## If the App Does Not Reflect Changes
|
|
109
|
+
|
|
110
|
+
- In development with a mounted volume: uvicorn `--reload` should reload automatically.
|
|
111
|
+
- If not: `docker compose restart app` or `docker compose up --build`.
|
|
112
|
+
- If dependencies changed: `docker compose up --build` to rebuild the image.
|