Lesson 04: Folder Structure
"Good organization is invisible until you need to find something."
📚 Table of Contents
- Introduction
- The Root Directory
- Backend Structure
- The App Directory
- Routers Explained
- Services Explained
- Supporting Files
- What Files NOT to Commit
- Evolution of Structure
- Best Practices
- Key Takeaways
- Interview Questions
- Exercises
- Next Steps
Introduction
A well-organized codebase is like a well-organized library — you can find what you need quickly.
In this lesson, you'll learn:
- Every directory and its purpose
- Every file and why it exists
- Naming conventions and why they matter
- What goes where when adding new features
- How the structure evolved and why
By the end, you'll navigate the codebase confidently and know exactly where to place new code.
The Root Directory
VeyraOps_AI/
├── backend/ # Python FastAPI server
├── frontend/ # Next.js dashboard
├── docs/ # Documentation (including this course)
├── learn_backend/ # Learning sandbox (not production)
├── learn_frontend/ # Learning sandbox (not production)
└── README.md # Project introductionWhy This Structure?
Monorepo: Frontend and backend in one repository
Benefits:
- ✅ Coordinated changes (update API and UI together)
- ✅ Shared documentation
- ✅ Single source of truth
Alternatives Considered:
- Separate repos: More complexity, harder to coordinate
- Nested structure:
backend/insidefrontend/— confusing
Backend Structure
backend/
├── app/ # Main application code
│ ├── routers/ # API endpoints
│ ├── services/ # Business logic
│ ├── main.py # FastAPI app entry point
│ ├── database.py # Database configuration
│ ├── models.py # SQLAlchemy ORM models
│ ├── schemas.py # Pydantic validation models
│ ├── agentops.db # SQLite database file
│ └── chroma_db/ # ChromaDB vector store
│ ├── chroma.sqlite3
│ └── [collection folders]
│
├── test/ # Test files
│ ├── seed_test_data.py
│ ├── test_messages.py
│ └── practice_*.py
│
├── docs/ # Backend-specific documentation
│
├── .env # Environment variables (NEVER commit!)
├── .gitignore # Git ignore rules
├── .python-version # Python version (3.14)
├── main.py # Backward compatibility entry
├── pyproject.toml # Project metadata (uv)
├── requirements.txt # Legacy dependencies list
├── uv.lock # Locked dependencies (uv)
└── README.md # Backend READMEThe App Directory
The app/ folder contains all production code.
Why "app"?
Convention: Python web frameworks typically use app/ or src/
Imports: Allows clean imports like from app.models import Agent
Structure
app/
├── __pycache__/ # Python compiled bytecode (ignored by git)
├── routers/ # HTTP endpoint definitions
│ ├── __pycache__/
│ ├── agents.py # Agent-related endpoints
│ └── documents.py # Document/RAG endpoints
│
├── services/ # Business logic layer
│ ├── __pycache__/
│ ├── openai_service.py # OpenAI API wrapper
│ ├── monitoring_service.py # Cost tracking
│ ├── multi_agent_email_service.py # Email workflow
│ ├── document_service.py # Document processing
│ ├── rag_graph_service.py # LangGraph RAG
│ └── hr_policy.txt # Sample document
│
├── main.py # FastAPI application factory
├── database.py # SQLAlchemy setup
├── models.py # Database schema
├── schemas.py # API contracts
├── agentops.db # SQLite database
└── chroma_db/ # Vector databaseFile-by-File Breakdown
Core Files
app/main.py
Purpose: FastAPI application entry point
Contents:
from fastapi import FastAPI
from database import Base, engine, run_startup_migrations
import models
from routers import agents, documents
# Create database tables
Base.metadata.create_all(bind=engine)
run_startup_migrations()
# Initialize FastAPI app
app = FastAPI(
title="AgentOps AI",
description="Backend API for AgentOps AI project",
version="0.1.0"
)
# Register routers
app.include_router(agents.router)
app.include_router(documents.router)
# Health check endpoints
@app.get("/")
async def root():
return {"message": "AgentOps AI backend is running"}
@app.get("/health")
async def health_check():
return {"status": "ok", "service": "agentops-ai-backend"}What It Does:
- Creates all database tables on startup
- Runs schema migrations
- Initializes FastAPI with metadata
- Registers all routers
- Provides health check endpoints
Why It's Small:
- Business logic lives in
services/ - API routes live in
routers/ - Database definitions live in
models.py
app/database.py
Purpose: Database connection and session management
Key Components:
DATABASE_URL = "sqlite:///./agentops.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def run_startup_migrations():
"""Apply SQLite schema updates"""
# Adds columns that create_all won't handle
def get_db():
"""Dependency injection for database sessions"""
db = SessionLocal()
try:
yield db
finally:
db.close()What Each Does:
engine: Database connection poolSessionLocal: Session factory for creating db sessionsBase: Parent class for all ORM modelsrun_startup_migrations(): Ad-hoc schema migrationsget_db(): FastAPI dependency for injecting sessions
Learn more in Lesson 05: Database Layer
app/models.py
Purpose: Define database schema using SQLAlchemy ORM
Contents (9 models):
Agent— AI agents configurationMessage— Chat message historyAgentRun— Execution trackingTask— Generated tasksToolCall— LLM operation loggingDocument— Uploaded filesDocumentChunk— Document segmentsWorkflowRun— Multi-agent workflow trackingWorkflowStep— Individual workflow steps
Example:
class Agent(Base):
__tablename__ = "agents"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
system_prompt = Column(Text, nullable=False)
enabled_tools = Column(Text, default="")
messages = relationship("Message", back_populates="agent", cascade="all, delete")
runs = relationship("AgentRun", back_populates="agent", cascade="all, delete")Learn more in Lesson 06: Models
app/schemas.py
Purpose: Pydantic models for request/response validation
Contents (25+ schemas):
- Request models (e.g.,
AgentCreate,ChatRequest) - Response models (e.g.,
AgentResponse,ChatResponse) - Nested models (e.g.,
RAGSource,WorkflowReviewResponse)
Example:
class ChatRequest(BaseModel):
message: str
class ChatResponse(BaseModel):
agent_id: int
user_message: str
assistant_response: str
run_id: int
latency_ms: intLearn more in Lesson 07: Schemas
Routers Explained
app/routers/agents.py (28,861 bytes!)
Purpose: All agent-related endpoints
Endpoints (13 total):
POST /agents/— Create agentGET /agents/— List all agentsGET /agents/{id}— Get single agentPOST /agents/{id}/chat— Chat with agentGET /agents/{id}/runs— Get agent runsPOST /agents/{id}/email/analyze— Analyze emailGET /agents/{id}/tasks— Get tasksPUT /tasks/{id}/status— Update task statusGET /agents/{id}/tool-calls— Get tool call logsPOST /agents/{id}/workflows/email— Multi-agent email workflowGET /agents/{id}/workflow-runs— Get workflow runsGET /workflow-runs/{id}/steps— Get workflow stepsGET /agents/{id}/dashboard— Dashboard metrics
Helper Functions:
normalize_due_date()— Parse and validate datesagent_has_tool()— Check if tool is enabledlog_tool_call()— Record LLM operationscreate_workflow_run()— Initialize workflow trackinglog_workflow_step()— Record workflow stepssafe_float(),safe_int()— Safe type conversion
Learn more in Lesson 09: API Routers
app/routers/documents.py (8,303 bytes)
Purpose: Document upload and RAG endpoints
Endpoints (5 total):
POST /agents/{id}/documents/upload— Upload documentGET /agents/{id}/documents— List documentsGET /agents/{id}/chunks— List chunksPOST /agents/{id}/documents/search— Search documentsPOST /agents/{id}/documents/ask— RAG query
Why Separate Router?:
- Document operations are logically distinct
- Easier to maintain
- Could be split into microservice later
Services Explained
app/services/openai_service.py
Purpose: Wrapper for OpenAI API calls
Functions (4 total):
generate_agent_response()— Chat completionanalyze_email()— Email analysis with structured outputgenerate_rag_answer()— Generate answer from contextevaluate_retrieved_context()— Check if context contains answer
Why This Exists:
- Centralized OpenAI configuration
- Consistent error handling
- Easy to mock for testing
- Can swap providers without changing routers
app/services/monitoring_service.py
Purpose: Cost and token tracking
Functions (2 total):
estimate_tokens()— Rough token count (1 token ≈ 4 chars)estimate_openai_cost()— Calculate approximate cost
Why Rough Estimates?:
- Exact tokenization requires
tiktokenlibrary - Estimates sufficient for monitoring
- Can be improved later
app/services/multi_agent_email_service.py
Purpose: Multi-agent workflow orchestration
Functions (3 agents):
run_analysis_agent()— Extract structured data from emailrun_reply_agent()— Generate professional replyrun_reviewer_agent()— Check quality and approve
Pattern: Each agent is a separate function returning data
app/services/document_service.py
Purpose: Document processing and vector search
Functions (5 total):
split_text_into_chunks()— Break documents into segmentscreate_embedding()— Generate vector embeddingadd_chunk_to_vector_store()— Save to ChromaDBsearch_agent_documents()— Semantic search
Why Chunking?:
- LLMs have context limits
- Embeddings work better on small segments
- Improves retrieval relevance
app/services/rag_graph_service.py
Purpose: LangGraph-based corrective RAG
Components:
RAGGraphState— TypedDict defining stateretrieve_node()— Fetch relevant chunksevaluate_node()— Check if answer existsanswer_node()— Generate answerrefusal_node()— Return "don't know"build_corrective_rag_graph()— Construct state machine
Why LangGraph?:
- State management for multi-step workflows
- Conditional routing based on evaluation
- Easier to debug than nested functions
Supporting Files
Configuration Files
.env
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small⚠️ NEVER COMMIT THIS FILE
pyproject.toml
[project]
name = "agentops-backend"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"fastapi",
"uvicorn",
"sqlalchemy",
"pydantic",
"openai",
"chromadb",
"langgraph"
]Purpose: Modern Python project metadata (used by uv)
requirements.txt
fastapi>=0.136.3
uvicorn>=0.48.0
sqlalchemy>=2.0.50Purpose: Legacy dependency format (for pip)
.gitignore
__pycache__/
*.pyc
.env
agentops.db
chroma_db/
.venv/Purpose: Tell Git which files to ignore
Database Files (NOT in Git)
app/agentops.db
- SQLite database file
- Created automatically on first run
- Contains all application data
- Size: Grows with usage
app/chroma_db/
- ChromaDB persistent storage
- Contains embeddings and metadata
- Size: Grows with document uploads
Why Not in Git?:
- Binary files (not text)
- User-specific data
- Large size
- Regenerated on each install
What Files NOT to Commit
Never Commit
.env # Contains secrets
agentops.db # User data
chroma_db/ # Binary vector store
__pycache__/ # Python compiled code
*.pyc # Python bytecode
.venv/ # Virtual environment
.python-version # May differ per developerAlways Commit
app/*.py # Source code
pyproject.toml # Dependencies
.gitignore # Ignore rules
README.md # Documentation
requirements.txt # Pip dependencies
uv.lock # Locked versionsEvolution of Structure
Version 1: Single File
# main.py (everything in one file)
from fastapi import FastAPI
from openai import OpenAI
app = FastAPI()
@app.post("/chat")
def chat():
# Database code here
# OpenAI code here
# Business logic hereProblems:
- Hard to find code
- Hard to test
- Hard to reuse
Version 2: Basic Split
app/
├── main.py # Routers
├── database.py # Database
└── models.py # ModelsBetter, but:
- All routers in one file (gets huge)
- Business logic mixed with HTTP
Version 3: Current Structure
app/
├── routers/ # HTTP layer
├── services/ # Business logic
├── main.py # Entry point
├── database.py # Database config
├── models.py # Schema
└── schemas.py # ValidationBenefits:
- Clear separation
- Easy to navigate
- Testable components
Future: Microservices?
services/
├── agent-service/
├── document-service/
├── workflow-service/
└── api-gateway/When?: If system grows to 100K+ users
Best Practices
Naming Conventions
# Files: snake_case
openai_service.py
multi_agent_email_service.py
# Classes: PascalCase
class Agent(Base):
class ChatRequest(BaseModel):
# Functions: snake_case
def generate_agent_response():
def log_tool_call():
# Constants: UPPER_SNAKE_CASE
OPENAI_MODEL = "gpt-4o-mini"
DATABASE_URL = "sqlite:///./agentops.db"File Size Guidelines
< 200 lines Good (easy to understand)
200-500 lines Acceptable (consider splitting)
500-1000 lines Warning (hard to navigate)
> 1000 lines Red flag (definitely split)Current Status:
agents.py: ~900 lines (should be split)documents.py: ~250 lines (acceptable)openai_service.py: ~200 lines (good)
Where to Add New Code
| Feature Type | Location |
|---|---|
| New API endpoint | routers/ |
| New business logic | services/ |
| New database table | models.py |
| New API schema | schemas.py |
| New external integration | services/new_service.py |
Key Takeaways
🎯 The root contains frontend, backend, and docs as a monorepo.
🎯 app/ is the production code with routers, services, models, and schemas.
🎯 Routers handle HTTP, services implement logic, models define schema.
🎯 Each service file wraps a specific external integration or workflow.
🎯 Database and vector store files are ignored by Git (generated at runtime).
🎯 .env contains secrets and must never be committed.
🎯 Structure evolved from single file → basic split → layered architecture.
🎯 Naming conventions follow Python standards (snake_case, PascalCase).
Interview Questions
Junior Level
Q1: What is the purpose of the app/ directory?
A: Contains all production application code (routers, services, models, schemas).
Q2: Why shouldn't you commit agentops.db to Git?
A: It's a binary file containing user-specific data that should be regenerated on each install.
Q3: Where would you add a new API endpoint?
A: In app/routers/, either in an existing file or a new router file.
Mid-Level
Q4: Explain the difference between models.py and schemas.py.
A: models.py defines database schema (SQLAlchemy ORM). schemas.py defines API contracts (Pydantic validation). Models are for database, schemas are for HTTP.
Q5: Why are services in a separate folder instead of in routers?
A: Separation of concerns. Services contain reusable business logic independent of HTTP. Makes testing easier and allows code reuse.
Q6: When would you create a new file in services/ vs adding to an existing one?
A: Create a new file when introducing a new external integration (e.g., anthropic_service.py) or a conceptually distinct workflow (e.g., video_processing_service.py).
Senior Level
Q7: The agents.py router is 900 lines. How would you refactor it?
A: Split by feature: agents_crud.py (create/read/update/delete), agents_chat.py (chat operations), agents_email.py (email workflows), agents_metrics.py (dashboard). Each router registers with a common prefix.
Q8: Design a folder structure if VeyraOps AI becomes a microservices architecture.
A: Separate repos or monorepo with: services/agent-service/, services/document-service/, services/workflow-service/, plus shared/models/, shared/schemas/, and api-gateway/. Each service has its own database and can deploy independently.
Q9: How would you organize configuration for multiple environments (dev/staging/prod)?
A: Use config/ folder with base.py, dev.py, staging.py, prod.py. Load based on ENV variable. Never commit secrets — use environment variables or secret managers (AWS Secrets Manager, Vault).
Exercises
Exercise 1: File Audit
Task: List every file in app/ and explain its purpose in one sentence.
Exercise 2: Refactoring Plan
Task: The agents.py router is too large. Design a refactoring strategy to split it into 3-4 smaller files.
Exercise 3: New Feature
Task: You need to add Slack integration. Where would you create files? What would you name them?
Exercise 4: Cleanup
Task: Identify all files in backend/ that should be in .gitignore but aren't.
Exercise 5: Documentation
Task: Create a docs/architecture.md file documenting the folder structure with diagrams.
Next Steps
You now understand every file and directory in the VeyraOps AI backend.
Next, you'll dive deep into database.py — understanding SQLAlchemy, sessions, and migrations.
← Previous: Request Lifecycle | ↑ Back to Index | Next: Database Layer →
Module 1 · Foundation
Last Updated: 2026-07-17