SHADOW AI

Up to Chapter 7, ACAI has evolved into a modular AI orchestration system:
Chapter 1 → Core API
Chapter 2 → Planner
Chapter 3 → Retrieval
Chapter 4 → Memory
Chapter 5 → Model Router
Chapter 6 → Verification
Chapter 7 → Workflow Engine
However, most of the current prototype state exists only while the application is running.
If the server stops:
Memory
↓
Lost
Workflow State
↓
Lost
Runtime Data
↓
Lost
A production system therefore needs persistent infrastructure.
Chapter 8 introduces:
Database
Persistent Memory
Workflow Persistence
Request IDs
Error Handling
Rate Limiting
Caching
Health Checks
Background Jobs
Production Configuration
The architecture now becomes:
USER
│
▼
API Gateway
│
▼
FastAPI Server
│
┌────────┴────────┐
▼ ▼
Rate Limiter Request ID
│ │
└────────┬────────┘
▼
ORCHESTRATOR
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
Planner Memory Retrieval
│ │ │
└────────────────────┼────────────────────┘
▼
Model Router
│
▼
Model Service
│
▼
Workflow
│
▼
Verification
│
▼
Response
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Database Cache Monitoring
Consider a user interaction:
User:
"I prefer Python."
ACAI stores it in memory.
Then the server restarts.
If the memory was only:
self.items = {}
the information disappears.
A persistent implementation changes this:
Memory
↓
Database
↓
Restart
↓
Database
↓
Memory restored
For the first production-oriented prototype, SQLite is a convenient starting point.
It requires no separate database server.
Create:
app/database.py
Add:
import sqlite3
from pathlib import Path
DATABASE_PATH = Path(
"data/acai.db"
)
def get_connection():
DATABASE_PATH.parent.mkdir(
parents=True,
exist_ok=True,
)
connection = sqlite3.connect(
DATABASE_PATH
)
connection.row_factory = (
sqlite3.Row
)
return connection
This creates:
data/
acai.db
Add:
def initialize_database():
connection = get_connection()
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS memories (
memory_id TEXT PRIMARY KEY,
content TEXT NOT NULL,
memory_type TEXT NOT NULL,
importance REAL NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS workflows (
workflow_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
result TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
connection.commit()
connection.close()
Update the FastAPI startup process.
For example:
from fastapi import FastAPI
from app.database import (
initialize_database,
)
app = FastAPI(
title="ACAI",
version="0.8.0",
)
@app.on_event("startup")
def startup():
initialize_database()
The database is initialized when the application starts.
The existing memory service can now write to SQLite.
Create:
app/services/persistent_memory.py
Add:
from datetime import datetime
from uuid import uuid4
from app.database import (
get_connection,
)
class PersistentMemoryService:
def remember(
self,
content: str,
memory_type: str = "general",
importance: float = 0.5,
) -> str:
memory_id = str(uuid4())
now = datetime.utcnow().isoformat()
connection = get_connection()
connection.execute(
"""
INSERT INTO memories (
memory_id,
content,
memory_type,
importance,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
memory_id,
content,
memory_type,
importance,
now,
now,
),
)
connection.commit()
connection.close()
return memory_id
Add:
def search(
self,
query: str,
limit: int = 5,
):
connection = get_connection()
rows = connection.execute(
"""
SELECT *
FROM memories
ORDER BY importance DESC
LIMIT ?
""",
(limit,),
).fetchall()
connection.close()
return [
dict(row)
for row in rows
]
This is a simple baseline.
A production semantic-memory system would later add embeddings and vector search.
The memory architecture is now:
MEMORY
│
┌────────┴────────┐
▼ ▼
Short-Term Long-Term
Memory Memory
│ │
│ ▼
│ Database
│ │
└────────┬────────┘
▼
Memory Context
Long-running workflows also need persistence.
Add:
def save_workflow(
workflow_id: str,
status: str,
result: str | None = None,
):
from datetime import datetime
now = datetime.utcnow().isoformat()
connection = get_connection()
connection.execute(
"""
INSERT INTO workflows (
workflow_id,
status,
result,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(workflow_id)
DO UPDATE SET
status = excluded.status,
result = excluded.result,
updated_at = excluded.updated_at
""",
(
workflow_id,
status,
result,
now,
now,
),
)
connection.commit()
connection.close()
Now a workflow can survive process restarts.
The persistent lifecycle becomes:
Created
↓
Saved
↓
Running
↓
Task A
↓
Task B
↓
Task C
↓
Completed
↓
Saved
If the process crashes:
Database
↓
Last known state
↓
Recovery
↓
Resume / Retry
A full recovery system requires additional task-level persistence, which can be added later.
Every API request should have a unique identifier.
Example:
request_id:
9c4d...
This makes debugging much easier.
Create:
import uuid
def generate_request_id():
return str(
uuid.uuid4()
)
In FastAPI:
from fastapi import Request
@app.middleware("http")
async def request_tracking(
request: Request,
call_next,
):
request_id = (
request.headers.get(
"X-Request-ID"
)
or generate_request_id()
)
response = await call_next(
request
)
response.headers[
"X-Request-ID"
] = request_id
return response
Now every response carries a request ID.
Suppose a user reports:
"My request failed."
Without an ID, debugging may require searching through thousands of logs.
With:
Request ID:
abc123
developers can search:
abc123
and find the complete request path.
Errors should be predictable.
Create:
app/errors.py
Add:
class ACAIError(Exception):
def __init__(
self,
message: str,
code: str = "ACAI_ERROR",
) -> None:
super().__init__(message)
self.message = message
self.code = code
Specific errors can inherit from it:
class ValidationError(ACAIError):
def __init__(
self,
message: str,
) -> None:
super().__init__(
message,
code="VALIDATION_ERROR",
)
class ModelError(ACAIError):
def __init__(
self,
message: str,
) -> None:
super().__init__(
message,
code="MODEL_ERROR",
)
class WorkflowError(ACAIError):
def __init__(
self,
message: str,
) -> None:
super().__init__(
message,
code="WORKFLOW_ERROR",
)
The API should return structured errors.
Example:
{
"success": false,
"error": {
"code": "MODEL_ERROR",
"message": "Model request failed.",
"request_id": "abc123"
}
}
Avoid exposing internal stack traces to normal users.
FastAPI can handle ACAI-specific errors:
from fastapi.responses import JSONResponse
@app.exception_handler(
ACAIError
)
async def acai_error_handler(
request,
exc: ACAIError,
):
request_id = (
request.headers.get(
"X-Request-ID",
"unknown",
)
)
return JSONResponse(
status_code=400,
content={
"success": False,
"error": {
"code": exc.code,
"message": exc.message,
"request_id": request_id,
},
},
)
Public APIs need protection against excessive requests.
Conceptually:
User
↓
Rate Limiter
↓
Allowed?
┌──┴──┐
YES NO
│ │
▼ ▼
API 429
A simple conceptual policy:
100 requests / minute / client
The actual production limit should be selected according to expected traffic and infrastructure capacity.
For demonstration:
import time
class RateLimiter:
def __init__(
self,
limit: int = 100,
window: int = 60,
):
self.limit = limit
self.window = window
self.requests = {}
def allow(
self,
client_id: str,
) -> bool:
now = time.time()
timestamps = (
self.requests
.setdefault(
client_id,
[],
)
)
timestamps[:] = [
timestamp
for timestamp in timestamps
if now - timestamp
< self.window
]
if len(timestamps) >= self.limit:
return False
timestamps.append(now)
return True
This is a prototype only.
For multiple application servers, a shared store such as Redis is more appropriate.
Some operations are repeated frequently.
For example:
Same query
↓
Same retrieval
↓
Repeated computation
A cache can reduce unnecessary work.
Architecture:
Request
↓
Cache
↓
┌───────┴───────┐
▼ ▼
HIT MISS
│ │
▼ ▼
Result Compute
│
▼
Cache
Create:
class SimpleCache:
def __init__(self):
self.data = {}
def get(
self,
key: str,
):
return self.data.get(
key
)
def set(
self,
key: str,
value,
):
self.data[key] = value
def delete(
self,
key: str,
):
self.data.pop(
key,
None,
)
def clear(self):
self.data.clear()
Not every operation should be cached.
Good candidates can include:
Stable retrieval results
Configuration
Public metadata
Repeated deterministic calculations
Potentially unsafe candidates include:
Private user data
Sensitive responses
Time-sensitive information
Requests whose result depends on changing state
A production cache therefore needs:
TTL
Invalidation
User isolation
Permission checks
Size limits
A production API should expose:
/health
Example:
@app.get("/health")
async def health():
return {
"status": "ok",
"service": "acai",
}
This confirms that the application process is responding.
These should eventually be separate.
"Is the application process alive?"
"Can the application actually serve requests?"
For example:
/health/live
/health/ready
Readiness can verify:
Database
Model provider
Required services
Configuration
Configuration should not be hardcoded.
Create:
.env
Example:
ACAI_ENV=development
ACAI_LOG_LEVEL=INFO
ACAI_DATABASE_URL=sqlite:///data/acai.db
ACAI_MODEL_PROVIDER=mock
ACAI_MAX_WORKFLOW_STEPS=10
Do not commit real secrets.
For example:
API_KEY=actual-secret
should never be placed into source control.
Update:
app/config.py
Example:
import os
class Settings:
environment = os.getenv(
"ACAI_ENV",
"development",
)
log_level = os.getenv(
"ACAI_LOG_LEVEL",
"INFO",
)
database_url = os.getenv(
"ACAI_DATABASE_URL",
"sqlite:///data/acai.db",
)
model_provider = os.getenv(
"ACAI_MODEL_PROVIDER",
"mock",
)
max_workflow_steps = int(
os.getenv(
"ACAI_MAX_WORKFLOW_STEPS",
"10",
)
)
settings = Settings()
Create:
import logging
logging.basicConfig(
level=logging.INFO,
)
logger = logging.getLogger(
"acai"
)
Then:
logger.info(
"Workflow started: %s",
workflow_id,
)
Errors:
logger.exception(
"Workflow failed: %s",
workflow_id,
)
ACAI
│
▼
Events
│
┌────────────┼────────────┐
▼ ▼ ▼
INFO ERROR METRICS
│ │ │
└────────────┼────────────┘
▼
Log System
Some tasks should not block the API request.
For example:
Large document processing
Long workflow
Batch evaluation
Embedding generation
Dataset indexing
Instead of:
HTTP Request
↓
Wait 10 minutes
↓
Response
use:
HTTP Request
↓
Create Job
↓
Return Job ID
Then:
Worker
↓
Process Job
↓
Save Result
The user can query:
/jobs/{job_id}
API
│
▼
Job Queue
│
┌────────┼────────┐
▼ ▼ ▼
Worker 1 Worker 2 Worker 3
│ │ │
└────────┼────────┘
▼
Database
│
▼
Result
For a simple prototype, FastAPI background tasks can be used.
For larger systems, a dedicated queue/worker architecture is more appropriate.
A typical deployment could be:
Internet
│
▼
Reverse Proxy
│
▼
FastAPI
│
├── Database
├── Cache
├── Model APIs
└── Workers
Multiple application instances can be added:
Load Balancer
/ | \
▼ ▼ ▼
API-1 API-2 API-3
\ | /
\ | /
Database
This is where shared state becomes important.
The API layer should ideally be stateless.
Instead of:
API Server
↓
Own local memory
use:
API Server
↓
Shared Database / Cache
Then any API instance can process the next request.
A production deployment can use Docker.
Example:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install \
--no-cache-dir \
-r requirements.txt
COPY app ./app
COPY data ./data
CMD [
"uvicorn",
"app.main:app",
"--host",
"0.0.0.0",
"--port",
"8000"
]
Build:
docker build -t acai .
Run:
docker run -p 8000:8000 acai
The exact deployment configuration depends on the target environment.
As the schema evolves, manually editing production tables becomes dangerous.
A production system should use migrations.
Conceptually:
Schema v1
↓
Migration 001
↓
Schema v2
↓
Migration 002
↓
Schema v3
This allows controlled database evolution.
ACAI should implement basic security before public deployment.
Important areas:
Authentication
Authorization
Input validation
Rate limiting
Secret management
TLS
Database permissions
Audit logging
Dependency updates
Never assume:
"AI application"
means security is automatically handled by the model.
Existing Pydantic validation should remain enabled.
Example:
class ChatRequest(BaseModel):
message: str = Field(
...,
min_length=1,
max_length=10000,
)
This prevents obviously invalid requests from entering the pipeline.
Every expensive operation should have a limit.
Examples:
Maximum input size
Maximum output tokens
Maximum workflow steps
Maximum retries
Maximum execution time
Maximum uploaded file size
Maximum concurrent jobs
This protects the system from accidental or malicious resource exhaustion.
After Chapter 8:
USER
│
▼
API Gateway
│
Rate Limiter
│
Request ID
│
▼
ORCHESTRATOR
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Planner Memory Retrieval
│
▼
Database
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Workflows Cache Logs
│
▼
Model Router
│
▼
Model Service
│
▼
Verification
│
▼
Result
Create:
tests/test_database.py
Example:
def test_database_initialization():
from app.database import (
initialize_database,
get_connection,
)
initialize_database()
connection = get_connection()
rows = connection.execute(
"""
SELECT name
FROM sqlite_master
WHERE type='table'
"""
).fetchall()
tables = {
row["name"]
for row in rows
}
assert "memories" in tables
assert "workflows" in tables
connection.close()
def test_persistent_memory():
from app.services.persistent_memory import (
PersistentMemoryService,
)
service = (
PersistentMemoryService()
)
memory_id = service.remember(
content=(
"The project uses "
"FastAPI."
),
memory_type="project",
importance=0.8,
)
assert memory_id
def test_rate_limiter():
from app.services.rate_limiter import (
RateLimiter,
)
limiter = RateLimiter(
limit=2,
window=60,
)
assert limiter.allow(
"client"
)
assert limiter.allow(
"client"
)
assert not limiter.allow(
"client"
)
def test_health(client):
response = client.get(
"/health"
)
assert response.status_code == 200
assert response.json()[
"status"
] == "ok"
Run:
pytest -v
For coverage:
pytest --cov=app
The exact coverage percentage should come from the actual test run.
Do not claim a coverage percentage without running the command.
Before deployment:
[ ] Environment variables configured
[ ] Secrets removed from source
[ ] Database persistence tested
[ ] Database backups planned
[ ] Rate limiting enabled
[ ] Input limits enabled
[ ] Request IDs implemented
[ ] Error handling implemented
[ ] Logging configured
[ ] Health checks available
[ ] Workflow limits configured
[ ] Retry limits configured
[ ] Timeout limits configured
[ ] Authentication implemented
[ ] Authorization implemented
[ ] Dependencies reviewed
[ ] HTTPS configured
[ ] Monitoring configured
[ ] Recovery strategy tested
A production architecture is not automatically production-ready simply because it contains:
Database
Docker
FastAPI
Redis
Workers
AI Models
Production readiness requires testing.
For example:
Architecture
↓
Implementation
↓
Unit Tests
↓
Integration Tests
↓
Load Tests
↓
Failure Tests
↓
Security Tests
↓
Monitoring
↓
Production
ACAI should deliberately test failures.
Examples:
Model unavailable
Database unavailable
Retrieval timeout
Worker crashes
Invalid model response
Rate limit reached
Workflow cycle
Memory database error
Network failure
The objective is to determine:
Does the system fail safely and predictably?
A useful experiment:
Start Workflow
↓
Execute Task A
↓
Crash Application
↓
Restart
↓
Read Persistent State
↓
Resume / Retry
If the workflow cannot recover, identify exactly where state was lost.
Chapter 8 is complete when:
[✓] Persistent database exists
[✓] Memory can be stored persistently
[✓] Workflow state can be persisted
[✓] Request IDs exist
[✓] Structured errors exist
[✓] Rate limiting is designed
[✓] Caching layer is defined
[✓] Health checks exist
[✓] Background jobs are defined
[✓] Configuration is externalized
[✓] Logging is implemented
[✓] Deployment architecture is defined
[✓] Infrastructure tests exist
At this point:
USER
│
▼
API GATEWAY
│
┌───────────┴───────────┐
▼ ▼
RATE LIMITER REQUEST ID
│ │
└───────────┬───────────┘
▼
ORCHESTRATOR
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
PLANNER MEMORY RETRIEVAL
│ │ │
│ ▼ │
│ DATABASE │
│ │ │
└────────────────────────┼────────────────────────┘
▼
MODEL ROUTER
│
▼
MODEL SERVICE
│
▼
WORKFLOW
│
▼
VERIFICATION
│
┌──────┴──────┐
▼ ▼
ACCEPT REVISE
│ │
└──────┬──────┘
▼
RESULT
│
┌────────────────┼────────────────┐
▼ ▼ ▼
CACHE LOGS METRICS
ACAI now has architecture, execution, persistence, and basic production infrastructure.
The next major step is to connect the system to real external model providers and tools while keeping the architecture provider-independent.
That leads to:
The next layer will cover:
Provider Interface
↓
OpenAI-compatible providers
↓
Cloud Models
↓
Local Models
↓
Fallback Models
↓
Timeouts
↓
Retries
↓
Provider Health
↓
Automatic Routing
The goal is not to claim that every provider will always work.
The goal is to design ACAI so that:
Provider A fails
↓
Health Check
↓
Fallback Policy
↓
Provider B
↓
Response
can happen in a controlled and measurable way.
End of Chapter 8
Comments
Post a Comment