Skip to content
Module 1 · FoundationBeginner12 min read · 2,304 words

Lesson 04: Folder Structure

"Good organization is invisible until you need to find something."


📚 Table of Contents

  1. Introduction
  2. The Root Directory
  3. Backend Structure
  4. The App Directory
  5. Routers Explained
  6. Services Explained
  7. Supporting Files
  8. What Files NOT to Commit
  9. Evolution of Structure
  10. Best Practices
  11. Key Takeaways
  12. Interview Questions
  13. Exercises
  14. 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 introduction

Why 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/ inside frontend/ — 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 README

The 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 database

File-by-File Breakdown

Core Files

app/main.py

Purpose: FastAPI application entry point

Contents:

python
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:

python
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 pool
  • SessionLocal: Session factory for creating db sessions
  • Base: Parent class for all ORM models
  • run_startup_migrations(): Ad-hoc schema migrations
  • get_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):

  1. Agent — AI agents configuration
  2. Message — Chat message history
  3. AgentRun — Execution tracking
  4. Task — Generated tasks
  5. ToolCall — LLM operation logging
  6. Document — Uploaded files
  7. DocumentChunk — Document segments
  8. WorkflowRun — Multi-agent workflow tracking
  9. WorkflowStep — Individual workflow steps

Example:

python
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:

python
class ChatRequest(BaseModel):
    message: str
 
class ChatResponse(BaseModel):
    agent_id: int
    user_message: str
    assistant_response: str
    run_id: int
    latency_ms: int

Learn more in Lesson 07: Schemas


Routers Explained

app/routers/agents.py (28,861 bytes!)

Purpose: All agent-related endpoints

Endpoints (13 total):

  1. POST /agents/ — Create agent
  2. GET /agents/ — List all agents
  3. GET /agents/{id} — Get single agent
  4. POST /agents/{id}/chat — Chat with agent
  5. GET /agents/{id}/runs — Get agent runs
  6. POST /agents/{id}/email/analyze — Analyze email
  7. GET /agents/{id}/tasks — Get tasks
  8. PUT /tasks/{id}/status — Update task status
  9. GET /agents/{id}/tool-calls — Get tool call logs
  10. POST /agents/{id}/workflows/email — Multi-agent email workflow
  11. GET /agents/{id}/workflow-runs — Get workflow runs
  12. GET /workflow-runs/{id}/steps — Get workflow steps
  13. GET /agents/{id}/dashboard — Dashboard metrics

Helper Functions:

  • normalize_due_date() — Parse and validate dates
  • agent_has_tool() — Check if tool is enabled
  • log_tool_call() — Record LLM operations
  • create_workflow_run() — Initialize workflow tracking
  • log_workflow_step() — Record workflow steps
  • safe_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):

  1. POST /agents/{id}/documents/upload — Upload document
  2. GET /agents/{id}/documents — List documents
  3. GET /agents/{id}/chunks — List chunks
  4. POST /agents/{id}/documents/search — Search documents
  5. POST /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):

  1. generate_agent_response() — Chat completion
  2. analyze_email() — Email analysis with structured output
  3. generate_rag_answer() — Generate answer from context
  4. evaluate_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):

  1. estimate_tokens() — Rough token count (1 token ≈ 4 chars)
  2. estimate_openai_cost() — Calculate approximate cost

Why Rough Estimates?:

  • Exact tokenization requires tiktoken library
  • Estimates sufficient for monitoring
  • Can be improved later

app/services/multi_agent_email_service.py

Purpose: Multi-agent workflow orchestration

Functions (3 agents):

  1. run_analysis_agent() — Extract structured data from email
  2. run_reply_agent() — Generate professional reply
  3. run_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):

  1. split_text_into_chunks() — Break documents into segments
  2. create_embedding() — Generate vector embedding
  3. add_chunk_to_vector_store() — Save to ChromaDB
  4. search_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 state
  • retrieve_node() — Fetch relevant chunks
  • evaluate_node() — Check if answer exists
  • answer_node() — Generate answer
  • refusal_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

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.50

Purpose: 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 developer

Always Commit

app/*.py              # Source code
pyproject.toml        # Dependencies
.gitignore            # Ignore rules
README.md             # Documentation
requirements.txt      # Pip dependencies
uv.lock               # Locked versions

Evolution of Structure

Version 1: Single File

python
# 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 here

Problems:

  • Hard to find code
  • Hard to test
  • Hard to reuse

Version 2: Basic Split

app/
├── main.py        # Routers
├── database.py    # Database
└── models.py      # Models

Better, 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      # Validation

Benefits:

  • 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

python
# 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 TypeLocation
New API endpointrouters/
New business logicservices/
New database tablemodels.py
New API schemaschemas.py
New external integrationservices/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.

👉 Lesson 05: Database Layer →


← Previous: Request Lifecycle | ↑ Back to Index | Next: Database Layer →


Module 1 · Foundation

Last Updated: 2026-07-17