AI Platform Architecture
The cross-cutting AI layer every intelligent feature in this app is built on: provider-agnostic LLM access, RAG, and seven AI-powered feature modules. Lives entirely under src/lib/ai/, deliberately mirroring src/lib/connectors/'s registry/manager/port pattern — the same shape already proven across four real broker integrations this codebase built earlier.
Layout
src/lib/ai/
sdk/ AIProvider interface, BaseAIProvider, StubAIProvider, types, errors
registry/ AIProviderId union, AI_PROVIDERS catalog, AIModelRegistry, AIProviderRegistry
manager/ AIProviderManager — resolve, call, fallback, retry, cost/usage recording
providers/ openai/, anthropic/, gemini/, ollama/ — one real adapter each
shared/ sse.ts, cost.ts, defaults.ts, rate-limit.ts, trading-heuristics.ts
services/ the 11 foundation services (below)
prompts/ code-defined default prompt templates, one file per feature
tools/ AIToolExecutionService's core tool registry
prop-firm/, journal/, coach/, risk-manager/, market/, chat/, automations/, dashboard/, knowledge/, security/
one thin module per feature — composes the services layer,
never reimplements it
register.ts registerOpenAIProvider() etc. — lazy, idempotent, env-gated
compose.ts createAIProviderManager(), createChatService(supabase)Foundation layer
`AIProvider` interface (sdk/provider.ts):
interface AIProvider {
readonly id: AIProviderId;
generate(req: AIGenerateRequest): Promise<AIGenerateResponse>;
stream(req: AIGenerateRequest): AsyncGenerator<AIStreamChunk>;
embed(req: AIEmbeddingRequest): Promise<AIEmbeddingResponse>;
listModels(): AIModelInfo[];
healthCheck(): Promise<AIProviderHealth>;
}BaseAIProvider (abstract) wraps generate/embed with metrics recording. StubAIProvider — what an unconfigured provider actually is, never a crash — throws AIProviderNotConfiguredError from every data method but returns a real 'unconfigured' status from healthCheck().
Registry — AIProviderRegistry creates a provider instance by id (real adapter if configured, StubAIProvider otherwise); AIModelRegistry resolves a model id to its provider + pricing/ capability metadata from the AI_PROVIDERS catalog (registry/providers.ts).
`AIProviderManager` (manager/ai-provider-manager.ts) — the one entry point every feature module calls through. Given a feature name and a model (with an optional fallback chain), it resolves the provider, calls it, and on a retryable failure falls back to the next model in the chain before giving up. Records one ai_usage_events row per attempt via a required, constructor-injected AIUsageRecorderPort (SupabaseUsageRecorder in production) — provider, model, feature, tokens, cost, latency, success/failure.
The 11 foundation services (services/*.ts)
| Service | Responsibility |
|---|---|
AIConversationService | CRUD over ai_conversations/ai_messages via an injected persistence port |
AIContextService | Assembles a prompt-ready context bundle from *existing* domain queries (accounts, trades, risk rules, plan tier) |
AIEmbeddingService | Wraps manager.embed(), writes ai_embeddings |
AIRAGService | Embeds a query, calls match_ai_embeddings() (pgvector cosine search, permission-scoped), returns top-K chunks |
AIMemoryService | Durable per-user facts distilled from conversation history (ai_memories) |
AIPromptManager | Renders {{var}} templates — code default (prompts/*.ts) + optional DB override (ai_prompt_templates) |
AIToolExecutionService | Typed function-calling registry; every handler is bound (userId, args), always user-scoped |
AIStreamingService | Wraps an AsyncGenerator<AIStreamChunk> into a Next.js SSE Response (toSSEStreamFromGenerator, sseResponseHeaders) |
AIAnalyticsService | Aggregation queries over ai_usage_events for the admin Cost Dashboard |
AIRecommendationEngine | The shared "render prompt → call AI → parse → write to ai_insights" orchestrator every feature phase below calls into |
AICacheService (cache-service.ts) | Per-instance TTL cache (aiCache singleton) — bounds redundant identical calls within one request lifecycle |
checkAIRateLimit() (shared/rate-limit.ts) is not a service class — it reuses the existing checkRateLimit()/check_rate_limit() Postgres function with one bucket per AIFeature.
RAG and embeddings
First vector infrastructure in this codebase: create extension if not exists vector;, one fixed-width column ai_embeddings.embedding vector(1536). The whole platform is standardized on one embedding model, OpenAI's text-embedding-3-small (STANDARD_EMBEDDING_MODEL), so every row shares the same dimensionality regardless of which provider generated the surrounding chat completion.
ai_embeddings.owner_user_id is the permission boundary: null = a platform-wide document (docs, broker docs, prop-firm rules); non-null = scoped to that one user's own trading data. Enforced twice — RLS on the table, and match_ai_embeddings() (SECURITY DEFINER) reading auth.uid() internally, never from a caller-supplied parameter. AIRAGService must always be constructed with the caller's session-scoped Supabase client, never the service role — see docs/AI_SECURITY.md.
KnowledgeIngestionService (knowledge/ingestion-service.ts) chunks and embeds source text (chunkText()) for platform_doc/broker_doc/ prop_firm_rule rows.
Feature modules
| Feature | Module | UI |
|---|---|---|
| Prop Firm Assistant | ai/prop-firm/ — pure rule-engine.ts (drawdown incl. trailing high-water-mark, consistency, trading-days, profit-target) + assistant-service.ts | /dashboard/prop-firms |
| Trade Journal | ai/journal/ — pure metrics.ts (win rate, expectancy, profit factor, best/worst setups, discipline score) + analysis-service.ts + report-service.ts | /dashboard/journal |
| Performance Coach | ai/coach/coaching-service.ts | Coaching tab on /dashboard/ai-assistant |
| Risk Manager | ai/risk-manager/ — pure monitor-service.ts (loss/exposure/correlation/consecutive-losses/risk score) + automation-service.ts (lock/pause/reduce/notify) | AI Risk Monitor tab on /dashboard/risk-manager |
| Market Intelligence | ai/market/intelligence-service.ts — AI-native synthesis, real but unconfigured MarketDataProvider port | /dashboard/market + the dashboard's Market News widget |
| Chat Assistant | ai/chat/chat-service.ts — RAG + tools + streaming | Chat tab on /dashboard/ai-assistant; POST /api/ai/chat |
| Automations | ai/automations/automation-engine.ts | /dashboard/automations; GET /api/ai/automations/run |
| AI Dashboard | ai/dashboard/dashboard-service.ts — read-only aggregation, no AI calls on load | /dashboard/ai |
Every feature module composes the foundation services and AIRecommendationEngine; none re-implements prompt rendering, provider calls, or usage recording.
Chat orchestration (tool-calling + streaming)
ChatService.streamReply() composes AIContextService, AIRAGService, AIPromptManager, AIToolExecutionService, AIConversationService, and AIProviderManager.stream(). It streams the model's response; if the model requests tool calls, they're executed (non-streamed, typically fast DB reads) and a second round is streamed with the tool results appended — capped at MAX_TOOL_ROUNDS = 2 to guarantee termination. The common no-tool-needed case costs exactly one AI call.
Database (additive only)
supabase/migrations/20260807200000_ai_platform.sql (+ three small follow-ups) adds: ai_conversations, ai_messages, ai_usage_events, ai_embeddings, ai_prompt_templates, ai_memories, ai_coaching_goals, risk_monitor_events, risk_automation_rules, ai_automations, ai_journal_reports, ai_market_briefs; widens prop_firms (10 named firms reseeded) and ai_insights.insight_type. Every table has RLS, owner-scoped for user data, service-role-write for platform-generated content (usage events, market briefs).
What is honestly NOT built
- BYOK — every provider-selection seam (registry,
register.ts,
AIProviderManager) is shaped so a future org-supplied key can be swapped in at the registration call site, but only platform-owned keys are wired up today.
- Live market data —
MarketDataProvider(ai/market/types.ts) is a
real, documented port with an honestly unconfigured UnconfiguredMarketDataProvider behind it, mirroring RithmicUnimplementedTransport. Market Intelligence runs on the model's own training knowledge, explicitly caveated in every prompt.
- A background scheduler —
GET /api/ai/automations/runis an
external-cron endpoint (shared-secret header), not a persistent process, matching this app's documented deployment model (docs/ARCHITECTURE.md).