Skip to content

Commit 15195db

Browse files
committed
✨ feat: add strict agent base model for schemas
- introduce AgentBaseModel with strict validation defaults - relocate and harden schema unit tests under tests/unit_tests/common - document structured output guidance in AGENTS.md and README variants Resolves #9
1 parent 05b9baf commit 15195db

File tree

6 files changed

+138
-0
lines changed

6 files changed

+138
-0
lines changed

AGENTS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- Core agent logic lives in `src/react_agent/` (`graph.py` orchestrates the ReAct loop); shared utilities reside in `src/common/`.
5+
- Shared Pydantic schemas should inherit from `src/common/basemodel.AgentBaseModel`; keep the base class in that module so `src/common/models/` stays focused on provider integrations.
6+
- Packaging metadata and templates are exported via `langgraph/templates/react_agent`; add new modules under `src/` and expose them through `__init__.py` as needed.
7+
- Tests are split into `tests/unit_tests/`, `tests/integration_tests/`, and `tests/e2e_tests/`; evaluation harnesses live in `tests/evaluations/` with scenario scripts.
8+
- Static assets such as screenshots or fixtures belong in `static/` or the relevant `tests/cassettes/` folder.
9+
10+
## Build, Test, and Development Commands
11+
- Install all deps with `uv sync --dev`; activate env through `uv run ...` or the generated `.venv`.
12+
- Launch the local graph runtime with `make dev` (headless) or `make dev_ui` (opens LangGraph Studio).
13+
- Run targeted suites via `make test_unit`, `make test_integration`, `make test_e2e`, or `make test_all`; watch mode is available through `make test_watch_*` targets.
14+
- Execute evaluation scenarios through `make eval_graph`, `make eval_multiturn`, or persona/model-specific targets like `make eval_graph_qwen`.
15+
16+
## Coding Style & Naming Conventions
17+
- Python 3.11+ with 4-space indentation; favor type annotations and Google-style docstrings (enforced by Ruff + pydocstyle).
18+
- Run `make lint` before committing; it invokes Ruff formatting/isort checks and strict MyPy over `src/`.
19+
- Use snake_case for modules and functions, PascalCase for classes, and uppercase ENV names; keep public tool IDs descriptive (e.g., `search_tool`).
20+
21+
## Testing Guidelines
22+
- Write tests with `pytest`; mirror implementation folders (e.g., `tests/unit_tests/common/` for `src/common/`).
23+
- Prefer deterministic fixtures and reuse cassette data in `tests/cassettes/` for external calls.
24+
- For new behaviors add unit coverage first, then integration/e2e if the agent flow changes; verify locally with the nearest `make test_*` target.
25+
26+
## Commit & Pull Request Guidelines
27+
- Follow the existing conventional-emoji prefix style (`📚 docs:`, `♻️ refactor:`); keep the summary imperative and under 72 chars.
28+
- Reference issues in the body (`Fixes #123`) and note evaluation/test results.
29+
- PRs should explain the change, list verification commands, and attach screenshots or trace links when UI or agent output changes.
30+
31+
## Security & Configuration Tips
32+
- Keep secrets in `.env` only; never commit API keys. Update `.env.example` when adding new configuration knobs.
33+
- Document any external service requirements (SiliconFlow, Tavily, OpenAI) and ensure fallback behaviors when keys are absent.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,26 @@ async def my_custom_tool(input: str) -> str:
185185
# Add to the tools list in get_tools()
186186
```
187187

188+
### Define Structured Outputs
189+
Use [`AgentBaseModel`](./src/common/basemodel.py) when you need structured responses (LLM tool outputs or graph state). It enforces strict typing, forbids unknown fields, and supports alias-driven serialization by default:
190+
191+
```python
192+
from pydantic import Field
193+
194+
from common import AgentBaseModel
195+
196+
197+
class Answer(AgentBaseModel):
198+
display_name: str = Field(alias="displayName")
199+
score: float
200+
201+
202+
result = Answer(display_name="Curie", score=0.98)
203+
assert result.model_dump(by_alias=True) == {"displayName": "Curie", "score": 0.98}
204+
```
205+
206+
Keep shared schema primitives in `src/common/basemodel.py` so provider integrations in `src/common/models/` stay focused on model client wiring.
207+
188208
### Add New MCP Tools
189209
Integrate external MCP servers for additional capabilities:
190210

@@ -330,6 +350,7 @@ The template uses a modular architecture:
330350

331351
Key components:
332352
- **`src/common/mcp.py`**: MCP client management for external documentation sources
353+
- **`src/common/basemodel.py`**: Centralizes Pydantic configuration via `AgentBaseModel` for structured tool outputs
333354
- **Dynamic tool loading**: Runtime tool selection based on context configuration
334355
- **Context system**: Centralized configuration with environment variable support
335356

README_CN.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,26 @@ async def my_custom_tool(input: str) -> str:
248248
return "工具输出"
249249
```
250250

251+
### 定义结构化输出
252+
当需要返回结构化响应(例如工具输出或图状态)时,请使用 [`AgentBaseModel`](./src/common/basemodel.py)。它默认启用严格类型检查、禁止未知字段,并支持字段别名序列化:
253+
254+
```python
255+
from pydantic import Field
256+
257+
from common import AgentBaseModel
258+
259+
260+
class Answer(AgentBaseModel):
261+
display_name: str = Field(alias="displayName")
262+
score: float
263+
264+
265+
result = Answer(display_name="居里", score=0.98)
266+
assert result.model_dump(by_alias=True) == {"displayName": "居里", "score": 0.98}
267+
```
268+
269+
共享的 schema 基类请放在 `src/common/basemodel.py` 中,以保持 `src/common/models/` 目录专注于模型客户端集成。
270+
251271
### 添加新的 MCP 工具
252272
集成外部 MCP 服务器以获得更多功能:
253273

src/common/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""Shared components for LangGraph agents."""
22

33
from . import prompts
4+
from .basemodel import AgentBaseModel
45
from .context import Context
56
from .models import create_qwen_model, create_siliconflow_model
67
from .tools import web_search
78
from .utils import load_chat_model
89

910
__all__ = [
1011
"Context",
12+
"AgentBaseModel",
1113
"create_qwen_model",
1214
"create_siliconflow_model",
1315
"web_search",

src/common/basemodel.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Shared Pydantic base classes for structured agent outputs."""
2+
3+
from __future__ import annotations
4+
5+
from pydantic import BaseModel, ConfigDict
6+
7+
8+
class AgentBaseModel(BaseModel):
9+
"""Base model for structured outputs returned by LangGraph agents.
10+
11+
This class centralizes common configuration for schemas that are exposed to
12+
LLM tool responses, ensuring consistent serialization and validation rules.
13+
Downstream models should inherit from this base rather than directly from
14+
``pydantic.BaseModel``.
15+
"""
16+
17+
model_config = ConfigDict(populate_by_name=True, extra="forbid", strict=True)
18+
19+
20+
__all__ = ["AgentBaseModel"]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Tests for the shared Pydantic base model used by the agent."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from pydantic import Field, ValidationError
7+
8+
from common import AgentBaseModel
9+
10+
11+
class Person(AgentBaseModel):
12+
"""Example structured response schema."""
13+
14+
name: str
15+
age: int
16+
17+
18+
def test_agent_base_model_enforces_types() -> None:
19+
result = Person(name="Ada", age=37)
20+
21+
assert result.name == "Ada"
22+
assert result.age == 37
23+
24+
with pytest.raises(ValidationError):
25+
Person(name="Ada", age="37")
26+
27+
28+
def test_agent_base_model_forbids_extra_fields() -> None:
29+
with pytest.raises(ValidationError):
30+
Person(name="Grace", age=35, nickname="hopper")
31+
32+
33+
def test_agent_base_model_populate_by_name() -> None:
34+
class AliasExample(AgentBaseModel):
35+
"""Model using aliases but still accepting field names."""
36+
37+
display_name: str = Field(alias="displayName")
38+
39+
result = AliasExample(display_name="Curie")
40+
41+
assert result.display_name == "Curie"
42+
assert result.model_dump(by_alias=True) == {"displayName": "Curie"}

0 commit comments

Comments
 (0)