Files
catonline_ai/vw-agentic-rag/docs/development.md
2025-09-26 17:15:54 +08:00

850 lines
19 KiB
Markdown

# 💻 Development Guide
This guide provides comprehensive information for developers working on the Agentic RAG system, including setup, code structure, development workflows, and best practices.
## Development Environment Setup
### Prerequisites
- **Python 3.12+** - [Download Python](https://www.python.org/downloads/)
- **Node.js 18+** - [Download Node.js](https://nodejs.org/)
- **uv** - Python package manager ([Install uv](https://github.com/astral-sh/uv))
- **Git** - Version control
- **VS Code** (recommended) - [Download VS Code](https://code.visualstudio.com/)
### Initial Setup
```bash
# Clone the repository
git clone <repository-url>
cd agentic-rag-4
# Install Python dependencies
uv sync --dev
# Install frontend dependencies
cd web && npm install
# Copy configuration template
cp config.yaml config.local.yaml
# Set up environment variables
export OPENAI_API_KEY="your-key"
export RETRIEVAL_API_KEY="your-key"
```
### VS Code Configuration
Recommended VS Code extensions:
```json
{
"recommendations": [
"ms-python.python",
"ms-python.black-formatter",
"charliermarsh.ruff",
"ms-python.mypy-type-checker",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next",
"esbenp.prettier-vscode"
]
}
```
Create `.vscode/settings.json`:
```json
{
"python.defaultInterpreterPath": "./.venv/bin/python",
"python.linting.enabled": true,
"python.linting.ruffEnabled": true,
"python.formatting.provider": "black",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["tests/"],
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"files.exclude": {
"**/__pycache__": true,
"**/.pytest_cache": true,
"**/.mypy_cache": true
}
}
```
## Architecture Deep Dive
### Backend Architecture (FastAPI + LangGraph)
```
service/
├── main.py # FastAPI application entry point
├── config.py # Configuration management
├── ai_sdk_adapter.py # Data Stream Protocol adapter
├── ai_sdk_chat.py # AI SDK compatible endpoints
├── llm_client.py # LLM provider abstractions
├── sse.py # Server-Sent Events utilities
├── graph/ # LangGraph workflow
│ ├── graph.py # Agent workflow definition
│ ├── state.py # State management (TurnState, AgentState)
│ └── message_trimmer.py # Context window management
├── memory/ # Session persistence
│ ├── postgresql_memory.py # PostgreSQL checkpointer
│ └── store.py # Memory abstractions
├── retrieval/ # Information retrieval
│ └── agentic_retrieval.py # Tool implementations
├── schemas/ # Data models
│ └── messages.py # Pydantic models
└── utils/ # Shared utilities
├── logging.py # Structured logging
└── templates.py # Prompt templates
```
### Frontend Architecture (Next.js + assistant-ui)
```
web/src/
├── app/
│ ├── layout.tsx # Root layout with providers
│ ├── page.tsx # Main chat interface
│ ├── globals.css # Global styles + assistant-ui
│ └── api/ # Server-side API routes
│ ├── chat/route.ts # Chat proxy endpoint
│ └── langgraph/ # LangGraph API proxy
├── components/ # Reusable components
├── hooks/ # Custom React hooks
└── lib/ # Utility libraries
```
## Development Workflow
### 1. Start Development Services
```bash
# Terminal 1: Start backend in development mode
make dev-backend
# or
./scripts/start_service.sh --dev
# Terminal 2: Start frontend development server
make dev-web
# or
cd web && npm run dev
# Alternative: Start both simultaneously
make dev
```
### 2. Development URLs
- **Backend API**: http://localhost:8000
- **API Documentation**: http://localhost:8000/docs
- **Frontend**: http://localhost:3000
- **Health Check**: http://localhost:8000/health
### 3. Hot Reloading
Both backend and frontend support hot reloading:
- **Backend**: uvicorn auto-reloads on Python file changes
- **Frontend**: Next.js hot-reloads on TypeScript/CSS changes
## Code Style and Standards
### Python Code Style
We use the following tools for Python code quality:
```bash
# Format code with Black
uv run black service/ tests/
# Lint with Ruff
uv run ruff check service/ tests/
# Type checking with MyPy
uv run mypy service/
# Run all quality checks
make lint
```
### Python Coding Standards
```python
# Example: Proper function documentation
async def stream_chat_response(request: ChatRequest) -> AsyncGenerator[str, None]:
"""
Stream chat response using agent workflow with PostgreSQL session memory.
Args:
request: Chat request containing messages and session_id
Yields:
str: SSE formatted events for streaming response
Raises:
HTTPException: If workflow execution fails
"""
try:
# Implementation...
pass
except Exception as e:
logger.error(f"Stream chat error: {e}", exc_info=True)
raise
```
### TypeScript/React Standards
```typescript
// Example: Proper component structure
interface ChatInterfaceProps {
sessionId?: string;
initialMessages?: Message[];
}
export function ChatInterface({
sessionId,
initialMessages = []
}: ChatInterfaceProps) {
// Component implementation...
}
```
### Configuration Management
Use environment-based configuration:
```python
# config.py example
from pydantic_settings import BaseSettings
from typing import Optional
class Config(BaseSettings):
provider: str = "openai"
openai_api_key: Optional[str] = None
retrieval_endpoint: str
class Config:
env_file = ".env"
env_prefix = "AGENTIC_"
```
## Testing Strategy
### Running Tests
```bash
# Run all tests
make test
# Run specific test types
make test-unit # Unit tests only
make test-integration # Integration tests only
make test-e2e # End-to-end tests
# Run with coverage
uv run pytest --cov=service --cov-report=html tests/
# Run specific test file
uv run pytest tests/unit/test_retrieval.py -v
# Run tests with debugging
uv run pytest -s -vvv tests/integration/test_api.py::test_chat_endpoint
```
### Test Structure
```
tests/
├── unit/ # Unit tests (fast, isolated)
│ ├── test_config.py
│ ├── test_retrieval.py
│ ├── test_memory.py
│ └── test_graph.py
├── integration/ # Integration tests (with dependencies)
│ ├── test_api.py
│ ├── test_streaming.py
│ ├── test_full_workflow.py
│ └── test_e2e_tool_ui.py
└── conftest.py # Shared test fixtures
```
### Writing Tests
```python
# Example unit test
import pytest
from service.retrieval.agentic_retrieval import RetrievalTool
class TestRetrievalTool:
@pytest.fixture
def tool(self):
return RetrievalTool(
endpoint="http://test-endpoint",
api_key="test-key"
)
async def test_search_standards(self, tool, httpx_mock):
# Mock HTTP response
httpx_mock.add_response(
url="http://test-endpoint/search",
json={"results": [{"title": "Test Standard"}]}
)
# Test the tool
result = await tool.search_standards("test query")
# Assertions
assert len(result["results"]) == 1
assert result["results"][0]["title"] == "Test Standard"
# Example integration test
class TestChatAPI:
@pytest.mark.asyncio
async def test_streaming_response(self, client):
request_data = {
"messages": [{"role": "user", "content": "test question"}],
"session_id": "test_session"
}
response = client.post("/api/chat", json=request_data)
assert response.status_code == 200
assert response.headers["content-type"] == "text/event-stream"
```
## API Development
### Adding New Endpoints
1. **Define the schema** in `service/schemas/`:
```python
# schemas/new_feature.py
from pydantic import BaseModel
from typing import List, Optional
class NewFeatureRequest(BaseModel):
query: str
options: Optional[List[str]] = []
class NewFeatureResponse(BaseModel):
result: str
metadata: dict
```
2. **Implement the logic** in appropriate module:
```python
# service/new_feature.py
async def process_new_feature(request: NewFeatureRequest) -> NewFeatureResponse:
# Implementation
return NewFeatureResponse(
result="processed",
metadata={"took_ms": 100}
)
```
3. **Add the endpoint** in `service/main.py`:
```python
@app.post("/api/new-feature")
async def new_feature_endpoint(request: NewFeatureRequest):
try:
result = await process_new_feature(request)
return result
except Exception as e:
logger.error(f"New feature error: {e}")
raise HTTPException(status_code=500, detail=str(e))
```
4. **Add tests**:
```python
# tests/unit/test_new_feature.py
def test_new_feature_endpoint(client):
response = client.post("/api/new-feature", json={
"query": "test",
"options": ["option1"]
})
assert response.status_code == 200
```
### LangGraph Agent Development
#### Adding New Tools
1. **Define the tool** in `service/retrieval/`:
```python
# agentic_retrieval.py
@tool
def new_search_tool(query: str, filters: Optional[dict] = None) -> dict:
"""
New search tool for specific domain.
Args:
query: Search query string
filters: Optional search filters
Returns:
Search results with metadata
"""
# Implementation
return {"results": [], "metadata": {}}
```
2. **Register the tool** in `service/graph/graph.py`:
```python
def build_graph() -> CompiledGraph:
# Add the new tool to tools list
tools = [
retrieve_standard_regulation,
retrieve_doc_chunk_standard_regulation,
new_search_tool # Add new tool
]
# Rest of graph building...
```
3. **Update the system prompt** to include the new tool:
```yaml
# config.yaml
llm:
rag:
agent_system_prompt: |
You have access to the following tools:
- retrieve_standard_regulation: Search standards/regulations
- retrieve_doc_chunk_standard_regulation: Search document chunks
- new_search_tool: Search specific domain
```
#### Modifying Agent Workflow
The agent workflow is defined in `service/graph/graph.py`:
```python
def agent_node(state: TurnState, config: RunnableConfig) -> TurnState:
"""Main agent decision-making node"""
# Get conversation history
messages = state.get("messages", [])
# Call LLM with tools
response = llm_with_tools.invoke(messages, config)
# Update state
new_messages = messages + [response]
return {"messages": new_messages}
def should_continue(state: TurnState) -> str:
"""Decide whether to continue or finish"""
last_message = state["messages"][-1]
# If LLM called tools, continue to tools
if last_message.tool_calls:
return "tools"
# Otherwise, finish
return "post_process"
```
## Frontend Development
### assistant-ui Integration
The frontend uses `@assistant-ui/react` for the chat interface:
```typescript
// app/page.tsx
import { Thread } from "@assistant-ui/react";
import { makeDataStreamRuntime } from "@assistant-ui/react-data-stream";
export default function ChatPage() {
const runtime = makeDataStreamRuntime({
api: "/api/chat",
});
return (
<div className="h-screen">
<Thread runtime={runtime} />
</div>
);
}
```
### Adding Custom Tool UI
```typescript
// components/ToolUI.tsx
import { ToolCall, ToolCallContent } from "@assistant-ui/react";
export function CustomToolUI() {
return (
<ToolCall toolName="retrieve_standard_regulation">
<ToolCallContent>
{({ result }) => (
<div className="border rounded p-4">
<h3>Search Results</h3>
{result?.results?.map((item, index) => (
<div key={index} className="mt-2">
<strong>{item.title}</strong>
<p>{item.description}</p>
</div>
))}
</div>
)}
</ToolCallContent>
</ToolCall>
);
}
```
### Styling with Tailwind CSS
The project uses Tailwind CSS with assistant-ui plugin:
```typescript
// tailwind.config.ts
import { assistant } from "@assistant-ui/react/tailwindcss";
export default {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [
assistant, // assistant-ui plugin
],
};
```
## Database Development
### Working with PostgreSQL Memory
The system uses PostgreSQL for session persistence via LangGraph's checkpointer:
```python
# memory/postgresql_memory.py
from langgraph.checkpoint.postgres import PostgresSaver
class PostgreSQLMemoryManager:
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.checkpointer = None
def get_checkpointer(self):
if not self.checkpointer:
self.checkpointer = PostgresSaver.from_conn_string(
self.connection_string
)
# Setup tables
self.checkpointer.setup()
return self.checkpointer
```
### Database Migrations
For schema changes, update the PostgreSQL setup:
```sql
-- migrations/001_add_metadata.sql
ALTER TABLE checkpoints
ADD COLUMN metadata JSONB DEFAULT '{}';
CREATE INDEX idx_checkpoints_metadata
ON checkpoints USING GIN (metadata);
```
## Debugging
### Backend Debugging
1. **Enable debug logging**:
```bash
export LOG_LEVEL=DEBUG
make dev-backend
```
2. **Use Python debugger**:
```python
# Add to code where you want to break
import pdb; pdb.set_trace()
# Or use breakpoint() in Python 3.7+
breakpoint()
```
3. **VS Code debugging**:
Create `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "FastAPI Debug",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/.venv/bin/uvicorn",
"args": [
"service.main:app",
"--reload",
"--host", "127.0.0.1",
"--port", "8000"
],
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceFolder}",
"LOG_LEVEL": "DEBUG"
}
}
]
}
```
### Frontend Debugging
1. **Browser DevTools**: Use React DevTools and Network tab
2. **Next.js debugging**:
```bash
# Start with debug mode
cd web && npm run dev -- --inspect
# Or use VS Code debugger
```
3. **Console logging**:
```typescript
// Add debug logs
console.log("Chat API request:", { messages, sessionId });
console.log("Backend response:", response);
```
## Performance Optimization
### Backend Performance
1. **Database connection pooling**:
```yaml
# config.yaml
postgresql:
pool_size: 20
max_overflow: 10
pool_timeout: 30
```
2. **Async request handling**:
```python
# Use async/await properly
async def handle_request():
# Good: concurrent execution
results = await asyncio.gather(
tool1.search(query),
tool2.search(query)
)
# Avoid: sequential execution
# result1 = await tool1.search(query)
# result2 = await tool2.search(query)
```
3. **Memory management**:
```python
# Limit conversation history
def trim_conversation(messages: List[Message], max_tokens: int = 32000):
# Implementation to keep conversations under token limit
pass
```
### Frontend Performance
1. **Code splitting**:
```typescript
// Lazy load components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
```
2. **Optimize bundle size**:
```bash
cd web && npm run build
npm run analyze # If you have bundle analyzer
```
## Common Development Tasks
### Adding Configuration Options
1. **Update config schema**:
```python
# config.py
class AppConfig(BaseSettings):
new_feature_enabled: bool = False
new_feature_timeout: int = 30
```
2. **Use in code**:
```python
config = get_config()
if config.app.new_feature_enabled:
# Feature implementation
pass
```
### Adding New Dependencies
1. **Python dependencies**:
```bash
# Add to pyproject.toml
uv add fastapi-users[sqlalchemy]
# For development dependencies
uv add --dev pytest-xdist
```
2. **Frontend dependencies**:
```bash
cd web
npm install @types/lodash
npm install --save-dev @testing-library/react
```
### Environment Management
Create environment-specific configs:
```bash
# Development
cp config.yaml config.dev.yaml
# Production
cp config.yaml config.prod.yaml
# Use specific config
export CONFIG_FILE=config.dev.yaml
make dev-backend
```
## Troubleshooting Development Issues
### Common Issues
1. **Port conflicts**:
```bash
# Check what's using port 8000
make port-check
# Kill processes on common ports
make port-kill
```
2. **Python import errors**:
```bash
# Ensure PYTHONPATH is set
export PYTHONPATH="${PWD}:${PYTHONPATH}"
# Or use uv run
uv run python -m service.main
```
3. **Database connection issues**:
```bash
# Test PostgreSQL connection
psql -h localhost -U user -d database -c "SELECT 1;"
# Check connection string format
echo $DATABASE_URL
```
4. **Frontend build errors**:
```bash
# Clear Next.js cache
cd web && rm -rf .next
# Reinstall dependencies
rm -rf node_modules package-lock.json
npm install
```
### Development Best Practices
1. **Use feature branches**:
```bash
git checkout -b feature/new-feature
# Make changes
git commit -m "Add new feature"
git push origin feature/new-feature
```
2. **Write tests first** (TDD approach):
```python
# Write test first
def test_new_feature():
assert new_feature("input") == "expected"
# Then implement
def new_feature(input: str) -> str:
return "expected"
```
3. **Keep commits small and focused**:
```bash
# Good commit messages
git commit -m "Add PostgreSQL connection pooling"
git commit -m "Fix citation parsing edge case"
git commit -m "Update frontend dependencies"
```
4. **Document as you go**:
```python
def complex_function(param: str) -> dict:
"""
Brief description of what this function does.
Args:
param: Description of parameter
Returns:
Description of return value
Example:
>>> result = complex_function("test")
>>> assert result["status"] == "success"
"""
```
---
This development guide provides the foundation for contributing to the Agentic RAG project. For specific questions or advanced topics, refer to the code comments and existing implementations as examples.