Compare commits
3 Commits
d82738e7ea
...
feature/ai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f3c63c4de | ||
|
|
aa089975df | ||
|
|
a6c04e52af |
@@ -1,139 +1,164 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
source ~/.nvm/nvm.sh && npm start # Dev with hot-reload
|
||||||
source ~/.nvm/nvm.sh && npm start # Start Electron app with hot-reload
|
|
||||||
|
|
||||||
# Build & Package
|
|
||||||
source ~/.nvm/nvm.sh && npm run make # Build distributable packages
|
source ~/.nvm/nvm.sh && npm run make # Build distributable packages
|
||||||
source ~/.nvm/nvm.sh && npm run package # Package without making installers
|
source ~/.nvm/nvm.sh && npm run package # Package without installers
|
||||||
|
source ~/.nvm/nvm.sh && npm run lint # ESLint (.ts/.tsx)
|
||||||
# Lint
|
source ~/.nvm/nvm.sh && npx drizzle-kit generate # Generate migration from schema
|
||||||
source ~/.nvm/nvm.sh && npm run lint # ESLint over .ts/.tsx files
|
|
||||||
|
|
||||||
# Database migrations (Drizzle)
|
|
||||||
source ~/.nvm/nvm.sh && npx drizzle-kit generate # Generate migration from schema changes
|
|
||||||
source ~/.nvm/nvm.sh && npx drizzle-kit push # Push schema directly (dev only)
|
source ~/.nvm/nvm.sh && npx drizzle-kit push # Push schema directly (dev only)
|
||||||
```
|
```
|
||||||
|
|
||||||
There is no test suite currently.
|
No test suite currently.
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture
|
||||||
|
|
||||||
Adiuva is a local-first Electron desktop app. The three Electron processes communicate via a custom tRPC↔IPC bridge (the public `electron-trpc` package is incompatible with tRPC v11, so a custom implementation is used).
|
Adiuva is a local-first Electron desktop app. The three processes communicate via a custom tRPC v11 ↔ IPC bridge (the public `electron-trpc` package is incompatible with tRPC v11).
|
||||||
|
|
||||||
### Process Boundaries
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Renderer (React) ──ipcLink──► Preload (contextBridge) ──IPC──► Main (tRPC router + SQLite)
|
Renderer (React 19) ──ipcLink──► Preload (contextBridge) ──IPC──► Main (tRPC router + SQLite)
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Main process** (`src/main/`) — Node.js, owns the database and all business logic
|
### Main Process (`src/main/`)
|
||||||
- `index.ts` — Window creation, app lifecycle
|
|
||||||
- `ipc.ts` — Custom handler that bridges `ipcMain` to tRPC procedures
|
|
||||||
- `router/index.ts` — All tRPC routers (clients, projects, tasks, checkpoints, notes, settings, ai)
|
|
||||||
- `db/index.ts` — Drizzle + better-sqlite3, WAL mode, singleton `getDb()`
|
|
||||||
- `db/schema.ts` — All table definitions (clients, projects, tasks, checkpoints, notes)
|
|
||||||
- `store.ts` — electron-store for persistent UI settings (e.g., `sidebarCollapsed`)
|
|
||||||
|
|
||||||
2. **Preload** (`src/preload/trpc.ts`) — Exposes `window.electronTRPC` with `sendMessage()` / `onMessage()`
|
Owns the database and all business logic.
|
||||||
|
|
||||||
3. **Renderer** (`src/renderer/`) — React 19, never accesses Node APIs directly
|
| File | Purpose |
|
||||||
- `lib/ipcLink.ts` — Custom TRPCLink that routes calls through `window.electronTRPC`
|
|---|---|
|
||||||
- `lib/trpc.ts` — `createTRPCReact<AppRouter>()` typed client
|
| `index.ts` | Window creation, app lifecycle |
|
||||||
- `index.tsx` — QueryClient + tRPC + Router providers
|
| `ipc.ts` | Bridges `ipcMain` to tRPC procedures |
|
||||||
- All data access is through `trpc.*.*useQuery()` / `trpc.*.*.useMutation()`
|
| `router/index.ts` | All tRPC sub-routers merged into `appRouter` |
|
||||||
|
| `db/index.ts` | Drizzle + better-sqlite3, WAL mode, singleton `getDb()` |
|
||||||
|
| `db/schema.ts` | Table definitions: clients, projects, tasks, checkpoints, notes, taskComments |
|
||||||
|
| `db/vectordb.ts` | LanceDB vector store for note embeddings |
|
||||||
|
| `store.ts` | electron-store for persistent UI settings |
|
||||||
|
|
||||||
|
### Preload (`src/preload/trpc.ts`)
|
||||||
|
|
||||||
|
Exposes `window.electronTRPC` with `sendMessage()` / `onMessage()`.
|
||||||
|
|
||||||
|
### Renderer (`src/renderer/`)
|
||||||
|
|
||||||
|
React 19 — never accesses Node APIs directly. All data through `trpc.*.useQuery()` / `trpc.*.useMutation()`.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `lib/ipcLink.ts` | Custom TRPCLink routing through `window.electronTRPC` |
|
||||||
|
| `lib/trpc.ts` | `createTRPCReact<AppRouter>()` typed client |
|
||||||
|
| `index.tsx` | QueryClient + tRPC + Router providers |
|
||||||
|
|
||||||
### Routing
|
### Routing
|
||||||
|
|
||||||
File-based routing via TanStack Router. Add a file to `src/renderer/routes/` and the route tree (`src/renderer/routeTree.gen.ts`) is auto-regenerated by the Vite plugin on next `npm start`. Routes:
|
File-based via TanStack Router (`tsr.config.json` at root). Route tree auto-generated at `routeTree.gen.ts`.
|
||||||
- `__root.tsx` — Root layout wrapping everything in `AppShell`
|
|
||||||
- `index.tsx`, `tasks.tsx`, `timeline.tsx`, `projects.tsx`
|
Routes: `__root.tsx` (AppShell layout), `index`, `tasks`, `timeline`, `projects`, `notes.$noteId`
|
||||||
|
|
||||||
|
### tRPC Routers
|
||||||
|
|
||||||
|
`health`, `settings`, `clients`, `projects`, `tasks`, `checkpoints`, `notes`, `taskComments`, `ai`
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
Schema lives in `src/main/db/schema.ts`. Migrations are in `src/main/db/migrations/`. The DB is created in Electron's `userData` directory as `adiuva.db`. On startup, `initDb()` runs non-destructive migrations (CREATE TABLE IF NOT EXISTS).
|
Schema in `src/main/db/schema.ts`, migrations in `src/main/db/migrations/`. DB created in Electron's `userData` as `adiuva.db`. On startup, `initDb()` runs non-destructive migrations.
|
||||||
|
|
||||||
To add a new table or column: edit `schema.ts`, run `drizzle-kit generate`, then `drizzle-kit push` (dev) or commit the migration file.
|
To add a table/column: edit `schema.ts` → `drizzle-kit generate` → `drizzle-kit push` (dev) or commit the migration.
|
||||||
|
|
||||||
### Adding a New Feature (end-to-end pattern)
|
### Adding a Feature (end-to-end)
|
||||||
|
|
||||||
1. **Schema** — Add table/columns to `src/main/db/schema.ts`
|
1. **Schema** — `src/main/db/schema.ts`
|
||||||
2. **Router** — Add a tRPC sub-router in `src/main/router/index.ts`, merge it into `appRouter`
|
2. **Router** — Add sub-router in `src/main/router/index.ts`, merge into `appRouter`
|
||||||
3. **Types** — `AppRouter` is exported from `src/main/router/index.ts` and imported in `src/renderer/lib/trpc.ts` — types flow automatically
|
3. **Types** — Flow automatically via `AppRouter` export
|
||||||
4. **UI** — Create components under `src/renderer/components/<feature>/`, use `trpc.*.*useQuery()` for data
|
4. **UI** — Components in `src/renderer/components/<feature>/`, data via `trpc.*.useQuery()`
|
||||||
|
|
||||||
### AI Subsystem (`src/main/ai/`)
|
## AI Subsystem (`src/main/ai/`)
|
||||||
|
|
||||||
LangGraph-based agentic system with pluggable LLM providers (OpenAI, Anthropic, GitHub Copilot).
|
LangGraph-based agentic system with pluggable LLM providers.
|
||||||
|
|
||||||
**Orchestrator** (`orchestrator.ts`): Classifies user intent → routes to one of three specialist agents:
|
### Orchestrator (`orchestrator.ts`)
|
||||||
- **Project agent** — project-scoped Q&A with tools: `read_project_notes`, `add_task`, `get_summary`, `suggest_checkpoints`, `suggest_tasks`
|
|
||||||
- **Knowledge agent** — cross-project semantic search via `vector_search_all`
|
|
||||||
- **General agent** — workspace-wide `add_task`
|
|
||||||
|
|
||||||
Tool-calling strategy differs by provider: OpenAI/Anthropic use LangChain `bindTools()` + ToolMessage loop (max 5 iterations); Copilot uses SDK-native tools (loop handled internally).
|
Classifies user intent → routes to a specialist agent:
|
||||||
|
|
||||||
**Streaming**: Orchestrator calls `sendStreamChunk(sender, token, done)` over IPC channel `'ai:stream'`. Renderer subscribes via `window.electronAI.onStreamChunk()` in `AIChatPanel.tsx`. `<tool_call>` blocks are filtered before sending to renderer.
|
| Agent | Scope | Tools |
|
||||||
|
|---|---|---|
|
||||||
|
| Project | Project-scoped Q&A | `read_project_notes`, `add_task`, `get_summary`, `suggest_checkpoints`, `suggest_tasks` |
|
||||||
|
| Knowledge | Cross-project search | `vector_search_all` |
|
||||||
|
| General | Workspace-wide | `add_task` |
|
||||||
|
|
||||||
**Provider factory** (`llm.ts`): `gpt-4o-mini` (OpenAI), `claude-sonnet-4-20250514` (Anthropic), or ChatCopilot wrapper — all with `temperature: 0.3` and streaming enabled.
|
All providers use LangChain `bindTools()` + ToolMessage loop (max 5 iterations).
|
||||||
|
|
||||||
**Token storage** (`token.ts`) — three-tier fallback:
|
Also exports `dailyBrief()` for AI-generated daily summaries (`ai.dailyBrief` tRPC mutation).
|
||||||
1. keytar (OS keychain) — preferred, encrypted per-user
|
|
||||||
|
### Streaming
|
||||||
|
|
||||||
|
`sendStreamChunk(sender, token, done)` over IPC `'ai:stream'`. Renderer subscribes via `window.electronAI.onStreamChunk()` in `AIChatPanel.tsx`. `<tool_call>` blocks are filtered before display.
|
||||||
|
|
||||||
|
### Providers (`llm.ts`)
|
||||||
|
|
||||||
|
| Provider | Model | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| OpenAI | `gpt-4o-mini` | Via LangChain |
|
||||||
|
| Anthropic | `claude-sonnet-4-20250514` | Via LangChain |
|
||||||
|
| Copilot | `ChatCopilot` wrapper | `copilot.ts` / `chat-copilot.ts` |
|
||||||
|
|
||||||
|
All use `temperature: 0.3`, streaming enabled. Provider management in `provider.ts`.
|
||||||
|
|
||||||
|
### Token Storage (`token.ts`)
|
||||||
|
|
||||||
|
Three-tier fallback (keytar service name: `'adiuva'`):
|
||||||
|
1. keytar (OS keychain) — preferred; `keytarFailed` flag skips after first failure
|
||||||
2. electron-store + `safeStorage` — encrypted at rest
|
2. electron-store + `safeStorage` — encrypted at rest
|
||||||
3. Plain electron-store — WSL fallback
|
3. Plain electron-store — WSL fallback
|
||||||
|
|
||||||
Keytar service name is `'adiuva'`. Once keytar fails, `keytarFailed` flag skips it for the session.
|
### Vector Embeddings (`db/vectordb.ts`)
|
||||||
|
|
||||||
**AI approval pattern**: Tasks and checkpoints have `isAiSuggested` (bool) and `isApproved` (bool) columns. AI-suggested items appear in the UI pending user approval before being treated as real records.
|
LanceDB in `{userData}/vectors/`. Schema: `{ id, projectId, content, vector }` (1536-dim, `text-embedding-3-small` via `embeddings.ts`). Embedding priority: Copilot CLI token → OpenAI token.
|
||||||
|
|
||||||
### Vector Embeddings (`src/main/db/vectordb.ts`)
|
- `upsertNoteEmbedding()` on note create/update (fire-and-forget)
|
||||||
|
- `migrateNotesIfNeeded()` backfills on first startup
|
||||||
|
- `searchNotes(query, limit=5)` used by Knowledge agent
|
||||||
|
|
||||||
LanceDB stored in `{userData}/vectors/`. Table schema: `{ id, projectId, content, vector }`. Vectors are 1536-dimensional (`text-embedding-3-small`). Embeddings use a priority chain: Copilot CLI token → OpenAI token.
|
### AI Approval Pattern
|
||||||
|
|
||||||
- Note create/update fires `upsertNoteEmbedding()` (fire-and-forget, errors swallowed)
|
Tasks and checkpoints have `isAiSuggested` + `isApproved` columns. AI suggestions appear pending user approval (dashed borders in UI).
|
||||||
- `migrateNotesIfNeeded()` backfills existing notes on first startup
|
|
||||||
- `searchNotes(query, limit=5)` is called by the Knowledge agent tool
|
|
||||||
|
|
||||||
### Key Config Notes
|
## Config Notes
|
||||||
|
|
||||||
- Vite configs use `.mts` extension (not `.ts`) to avoid ESM/CJS conflicts with electron-forge's externalize-deps plugin
|
- Vite configs use `.mts` (not `.ts`) — avoids ESM/CJS conflicts with electron-forge
|
||||||
- `@/*` path alias resolves to `src/renderer/*` (TypeScript + Vite + shadcn/ui all share this alias)
|
- `@/*` path alias → `src/renderer/*` (TypeScript + Vite + shadcn/ui)
|
||||||
- shadcn/ui style: **new-york**, base color: **neutral**
|
- **shadcn/ui**: new-york style, neutral base color
|
||||||
- Icons: **lucide-react** throughout — do not introduce other icon libraries
|
- **Icons**: lucide-react only — do not introduce other icon libraries
|
||||||
- Tailwind 4 (not 3) — use CSS variable theming via `globals.css`, not `tailwind.config.js`
|
- **Tailwind 4** — CSS variable theming in `globals.css`, no `tailwind.config.js`
|
||||||
- Notes use Milkdown (`@milkdown/crepe`) as the markdown editor (`src/renderer/components/notes/MilkdownEditor.tsx`)
|
- **Notes editor**: Milkdown (`@milkdown/crepe`) at `src/renderer/components/notes/MilkdownEditor.tsx`
|
||||||
- Routes: `index`, `tasks`, `timeline`, `projects`, `notes.$noteId` (note ID is a URL param)
|
|
||||||
|
|
||||||
## Design Context
|
## Design Context
|
||||||
|
|
||||||
### Users
|
### Target User
|
||||||
Freelancers and solo professionals managing their own client work — projects, tasks, notes, and timelines. They work alone and need a single workspace that keeps everything organized without the overhead of enterprise tools. The AI assistant is a force multiplier, helping them stay on top of their workload.
|
Freelancers and solo professionals managing client work (projects, tasks, notes, timelines). Single workspace, no enterprise overhead. AI as force multiplier.
|
||||||
|
|
||||||
### Brand Personality
|
### Brand
|
||||||
**Calm, intelligent, warm.** Adiuva is a thoughtful companion, not a flashy tool. It should feel like a well-organized desk — everything in its place, nothing competing for attention. The tone is confident and understated, never loud or gamified.
|
**Calm, intelligent, warm.** Thoughtful companion, not flashy tool. Confident and understated, never loud or gamified.
|
||||||
|
|
||||||
### Aesthetic Direction
|
### Palette
|
||||||
- **Visual tone**: Editorial, premium, content-first. Inspired by Notion's clean typography and warm neutrals, but with a distinct identity through the warm pinkish-white canvas and golden yellow accent
|
|
||||||
- **Light mode**: Soft and warm — pinkish-white (`#f4edf3`) canvas, golden yellow (`#fbc881`) primary, slate blue-gray (`#8a8ea9`) secondary, dusty lavender borders (`#c8c3cd`)
|
| | Canvas | Primary | Secondary | Borders |
|
||||||
- **Dark mode**: Stark monochrome — near-black canvas (`#0c0c0c`), crisp white text, dark gray surfaces (`#323232`). No color accent; primary is pure white
|
|---|---|---|---|---|
|
||||||
- **Typography**: Geist (geometric sans-serif) at 400/500/600. Tight tracking on large headings (`-1px`). Body at `text-sm`, metadata at `text-xs`
|
| **Light** | Pinkish-white `#f4edf3` | Golden yellow `#fbc881` | Slate blue-gray `#8a8ea9` | Dusty lavender `#c8c3cd` |
|
||||||
- **Corners**: 10px base radius, consistently rounded. Chat elements use `rounded-2xl`
|
| **Dark** | Near-black `#0c0c0c` | Pure white | — | Dark gray `#323232` |
|
||||||
- **Signature effects**: Glassmorphism on AI inputs/floating chat (`backdrop-blur-xl`, transparency). Spring physics animations (stiffness 400, damping 30). Subtle scale-and-fade transitions
|
|
||||||
- **Anti-references**: No gamification (badges, streaks, confetti). No corporate/enterprise density. Keep it mature and professional
|
### Typography
|
||||||
|
Geist sans-serif, weights 400/500/600. Tight tracking (`-1px`) on headings. Body `text-sm`, metadata `text-xs`.
|
||||||
|
|
||||||
|
### Visual Language
|
||||||
|
- 10px border-radius, `rounded-2xl` for chat elements
|
||||||
|
- Glassmorphism on AI inputs (`backdrop-blur-xl`, transparency)
|
||||||
|
- Spring animations (stiffness 400, damping 30), scale-and-fade transitions
|
||||||
|
- No gamification (badges, streaks, confetti). Mature and professional
|
||||||
|
|
||||||
### Design Principles
|
### Design Principles
|
||||||
|
1. **Clarity over cleverness** — Clear hierarchy, generous whitespace, comfortable density
|
||||||
1. **Clarity over cleverness** — Every element should communicate its purpose instantly. Prefer clear hierarchy and whitespace over decorative flourish. Information density should feel comfortable, not cramped.
|
2. **AI as quiet partner** — Deeply integrated but never intrusive. Dashed borders for pending AI items, Sparkles icon as AI marker
|
||||||
|
3. **Warmth in restraint** — Warm palette feels approachable without being playful. Dark mode trades warmth for focus
|
||||||
2. **AI as quiet partner** — The AI is deeply integrated (floating chat, suggestions) but never intrusive. AI-suggested items use dashed borders to signal "pending." The Sparkles icon is the consistent AI identity marker.
|
4. **Motion with purpose** — Animations reinforce spatial relationships, never decorative
|
||||||
|
5. **Confidence through consistency** — CSS variable tokens, shadcn/ui primitives, Geist font. Predictable, keyboard-first
|
||||||
3. **Warmth in restraint** — The palette is deliberately warm (pinkish whites, golden yellows) to feel approachable without being playful. Dark mode trades warmth for focus. Let the content breathe.
|
|
||||||
|
|
||||||
4. **Motion with purpose** — Spring physics and glassmorphism create a sense of physicality and depth. Animations should feel natural and responsive, never decorative or slow. Every transition should reinforce spatial relationships.
|
|
||||||
|
|
||||||
5. **Confidence through consistency** — Use the established token system (CSS variables, shadcn/ui primitives, Geist font). The user should feel in control — predictable patterns, keyboard-first interactions, no surprises.
|
|
||||||
|
|||||||
8
.claude/settings.json
Normal file
8
.claude/settings.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add AI_REFACTOR_PLAN.md)",
|
||||||
|
"Bash(git commit:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
389
AI_REFACTOR_PLAN.md
Normal file
389
AI_REFACTOR_PLAN.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# AI Refactor Plan — Adiuva Electron App
|
||||||
|
|
||||||
|
> **Objective:** Transform the Electron app from a single-process AI integration into a local-first multi-agent client with plugin-based batch agents, multi-provider LLM support, E2E encrypted backup, granular permissions, and cloud backend integration.
|
||||||
|
>
|
||||||
|
> **Backend:** Lives in a separate repository. See `BACKEND_PLAN.md` for the API contract and backend implementation guide.
|
||||||
|
>
|
||||||
|
> **Protocol:** Execute steps sequentially. Each step is atomic and committable. Mark `[x]` when done.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — API Contracts & Types
|
||||||
|
|
||||||
|
### Step 0.1 — Define backend API contract types
|
||||||
|
- [ ] Create `src/shared/api-types.ts` with all interfaces the Electron app needs to communicate with the backend:
|
||||||
|
- `ExecutionPlan`, `PlanStep`, `PlanAction` (action types: `create_record`, `update_record`, `delete_record`, `index_document`, `send_notification`)
|
||||||
|
- `ChatRequest` (message, context, execution_mode: `'direct'` | `'plan'`)
|
||||||
|
- `ChatResponse` (response, actions)
|
||||||
|
- `ChatContext` (user_profile, relevant_documents, recent_tasks, conversation_history)
|
||||||
|
- `AgentManifest` (name, description, permissions, schedule)
|
||||||
|
- `PermissionGrant` (plugin, permission type, resource path, granted_at)
|
||||||
|
- `BackupMetadata` (version, timestamp, checksum, chunk_count)
|
||||||
|
- `BillingTier` enum (`free`, `pro`, `power`, `team`)
|
||||||
|
- `AuthTokens` (access_token, refresh_token, expires_at)
|
||||||
|
- `UserProfile` (id, email, tier)
|
||||||
|
- [ ] Update `tsconfig.json` paths if needed to include `src/shared/`
|
||||||
|
- **Files:** `src/shared/api-types.ts`, `tsconfig.json`
|
||||||
|
- **Outcome:** Type-safe contracts for all backend communication. Backend repo mirrors these as Pydantic schemas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — LiteLLM Multi-Provider Client
|
||||||
|
|
||||||
|
### Step 1.1 — Create unified LLM client wrapper
|
||||||
|
- [ ] Create `src/main/llm/litellm-client.ts`:
|
||||||
|
- `LiteLLMClient` class with unified interface:
|
||||||
|
- `complete(messages: Message[], options?: CompletionOptions): Promise<CompletionResponse>`
|
||||||
|
- `stream(messages: Message[], options?: CompletionOptions): AsyncGenerator<string>`
|
||||||
|
- `embed(text: string): Promise<number[]>`
|
||||||
|
- `CompletionOptions`: model override, temperature, max_tokens, tools
|
||||||
|
- Provider-agnostic: internally maps to the correct provider SDK
|
||||||
|
- Fallback chain: tries primary provider, on failure tries secondary, logs each attempt
|
||||||
|
- Timeout handling: per-provider configurable timeouts
|
||||||
|
- [ ] Create `src/main/llm/providers.ts`:
|
||||||
|
- `ProviderConfig` interface: name, apiKey, model, endpoint (for Ollama), timeout, isLocal
|
||||||
|
- `ProviderRegistry`: manages configured providers, persists to electron-store
|
||||||
|
- `getActiveProvider()`, `setActiveProvider(name)`, `addProvider(config)`, `removeProvider(name)`
|
||||||
|
- `getFallbackChain(): ProviderConfig[]`
|
||||||
|
- Supported providers: OpenAI, Anthropic, Google (Gemini), Mistral, Groq, Ollama (local)
|
||||||
|
- [ ] Create `src/main/llm/embeddings.ts` (refactored):
|
||||||
|
- Support multiple embedding providers (OpenAI text-embedding-3-small, local ONNX with all-MiniLM-L6-v2)
|
||||||
|
- Auto-select: use local ONNX if available, fall back to API
|
||||||
|
- Same `embedText(text): Promise<number[]>` interface
|
||||||
|
- **Files:** `src/main/llm/litellm-client.ts`, `src/main/llm/providers.ts`, `src/main/llm/embeddings.ts`
|
||||||
|
- **Outcome:** Single LLM interface that all local components use. Supports 6+ providers with fallback.
|
||||||
|
|
||||||
|
### Step 1.2 — Migrate existing AI code to use new LLM client
|
||||||
|
- [ ] Update `src/main/ai/orchestrator.ts`:
|
||||||
|
- Replace direct `getLLM()` calls with `LiteLLMClient.complete()` / `LiteLLMClient.stream()`
|
||||||
|
- Keep local orchestration working with the new client (backend delegation comes in Phase 3)
|
||||||
|
- [ ] Update `src/main/ai/llm.ts`:
|
||||||
|
- Deprecate. Redirect `getLLM()` to instantiate via `LiteLLMClient` as a thin compatibility shim
|
||||||
|
- [ ] Update `src/main/ai/embeddings.ts` to delegate to `src/main/llm/embeddings.ts`
|
||||||
|
- [ ] Update `src/main/ai/token.ts`:
|
||||||
|
- Add `listStoredProviders(): Promise<string[]>` to enumerate which providers have tokens
|
||||||
|
- [ ] Ensure all existing AI features (chat, daily brief, tool calling) continue to work
|
||||||
|
- **Files:** `src/main/ai/orchestrator.ts`, `src/main/ai/llm.ts`, `src/main/ai/embeddings.ts`, `src/main/ai/token.ts`
|
||||||
|
- **Outcome:** Existing AI features work identically but go through the new unified LLM client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — Local Plugin System & Batch Agents
|
||||||
|
|
||||||
|
### Step 2.1 — Create plugin manifest system and permission manager
|
||||||
|
- [ ] Create `src/main/permissions/manifest-validator.ts`:
|
||||||
|
- `PluginManifest` interface: `name`, `description`, `version`, `permissions: PermissionRequest[]`, `schedule?: string` (cron), `entryPoint: string`
|
||||||
|
- `PermissionRequest`: `type` (read_folder, read_email, read_calendar, read_browser_history), `resource?: string` (path, account), `reason: string`
|
||||||
|
- `validateManifest(manifest): ValidationResult` — validates structure, checks for dangerous permissions
|
||||||
|
- [ ] Create `src/main/permissions/permission-manager.ts`:
|
||||||
|
- `PermissionManager` class (singleton):
|
||||||
|
- `grantPermission(pluginName, permission): void` — persists to SQLite
|
||||||
|
- `revokePermission(pluginName, permission): void`
|
||||||
|
- `checkPermission(pluginName, permission): boolean`
|
||||||
|
- `getPluginPermissions(pluginName): PermissionGrant[]`
|
||||||
|
- `getAllGrants(): PermissionGrant[]`
|
||||||
|
- `logAccess(pluginName, permission, resource, timestamp): void` — activity log
|
||||||
|
- `getActivityLog(pluginName?, limit?): ActivityLogEntry[]`
|
||||||
|
- Permission grants stored in a new `plugin_permissions` SQLite table
|
||||||
|
- Activity log stored in a new `plugin_activity_log` SQLite table
|
||||||
|
- [ ] Add `plugin_permissions` and `plugin_activity_log` tables to `src/main/db/schema.ts`
|
||||||
|
- [ ] Generate and apply migration
|
||||||
|
- **Files:** `src/main/permissions/manifest-validator.ts`, `src/main/permissions/permission-manager.ts`, `src/main/db/schema.ts`, `src/main/db/migrations/`
|
||||||
|
- **Outcome:** Granular, opt-in permission system for plugins. Every access is logged.
|
||||||
|
|
||||||
|
### Step 2.2 — Create worker pool and batch runner
|
||||||
|
- [ ] Create `src/main/workers/worker-pool.ts`:
|
||||||
|
- `WorkerPool` class:
|
||||||
|
- Manages a pool of Node.js `worker_threads`
|
||||||
|
- `runPlugin(manifest, context): Promise<PluginResult>` — spawns or reuses a worker, sends manifest + context, receives result
|
||||||
|
- Worker lifecycle: create, send message, receive result, terminate on timeout
|
||||||
|
- Max concurrent workers: configurable (default 4)
|
||||||
|
- Error isolation: worker crash doesn't affect main process
|
||||||
|
- [ ] Create `src/main/workers/batch-runner.ts`:
|
||||||
|
- `BatchRunner` class:
|
||||||
|
- `registerPlugin(manifest): void` — validates manifest, stores in registry
|
||||||
|
- `startScheduler(): void` — cron-based scheduler using `node-cron` or simple setInterval
|
||||||
|
- `runPlugin(name, triggerContext?): Promise<PluginResult>` — manual trigger
|
||||||
|
- `stopAll(): void` — graceful shutdown of all scheduled plugins
|
||||||
|
- Scheduler checks permissions before each run; skips if revoked
|
||||||
|
- Results logged to activity log
|
||||||
|
- [ ] Create `src/main/workers/plugin-worker.ts`:
|
||||||
|
- Worker thread entry point
|
||||||
|
- Receives plugin config + context via `parentPort.on('message')`
|
||||||
|
- Dynamically imports the plugin entry point
|
||||||
|
- Executes `run(context)` with sandboxed access (only permitted resources)
|
||||||
|
- Posts result back via `parentPort.postMessage()`
|
||||||
|
- **Files:** `src/main/workers/worker-pool.ts`, `src/main/workers/batch-runner.ts`, `src/main/workers/plugin-worker.ts`
|
||||||
|
- **Outcome:** Isolated plugin execution environment with scheduling, permissions enforcement, and error isolation.
|
||||||
|
|
||||||
|
### Step 2.3 — Implement batch agent plugins
|
||||||
|
- [ ] Create `src/plugins/email-scanner.ts`:
|
||||||
|
- Manifest: requires `read_email` permission
|
||||||
|
- Connects to IMAP via `imapflow` (account configured in settings)
|
||||||
|
- Scans for new emails since last run
|
||||||
|
- Uses `LiteLLMClient` to classify each email (has actionable task? extract title, priority, description)
|
||||||
|
- Returns extracted task metadata (never raw email content) for execution via backend or local playbook
|
||||||
|
- [ ] Create `src/plugins/file-watcher.ts`:
|
||||||
|
- Manifest: requires `read_folder` permission for each watched path
|
||||||
|
- Uses `chokidar` to watch approved directories
|
||||||
|
- On new/modified file: reads content, generates embedding, upserts into vector store
|
||||||
|
- Supports: .txt, .md, .pdf (text extraction), .docx (basic extraction)
|
||||||
|
- [ ] Create `src/plugins/calendar-sync.ts`:
|
||||||
|
- Manifest: requires `read_calendar` permission
|
||||||
|
- Parses ICS files or connects to CalDAV endpoint
|
||||||
|
- Detects scheduling conflicts
|
||||||
|
- Suggests reorganizations via LLM analysis
|
||||||
|
- Returns calendar events + conflict reports
|
||||||
|
- [ ] Create `src/plugins/browser-agent.ts`:
|
||||||
|
- Manifest: requires `read_browser_history` permission (explicit opt-in)
|
||||||
|
- Reads browser bookmarks and history from known browser paths (Chrome, Firefox, Edge)
|
||||||
|
- Indexes relevant entries into vector store
|
||||||
|
- Privacy-first: only indexes URLs and titles, not page content
|
||||||
|
- **Files:** `src/plugins/email-scanner.ts`, `src/plugins/file-watcher.ts`, `src/plugins/calendar-sync.ts`, `src/plugins/browser-agent.ts`
|
||||||
|
- **Outcome:** Four local batch agents running as isolated worker threads, using LiteLLM for analysis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3 — Backend Integration
|
||||||
|
|
||||||
|
### Step 3.1 — Create backend HTTP/WebSocket client
|
||||||
|
- [ ] Create `src/main/api/backend-client.ts`:
|
||||||
|
- `BackendClient` class:
|
||||||
|
- `baseUrl` configurable (default: production cloud URL, overridable for dev)
|
||||||
|
- `setAuthToken(jwt: string): void`
|
||||||
|
- `chat(request: ChatRequest): Promise<ChatResponse>` — POST /api/v1/chat
|
||||||
|
- `chatStream(request: ChatRequest): AsyncGenerator<string>` — WebSocket /api/v1/chat/stream
|
||||||
|
- `getPlaybooks(): Promise<ExecutionPlan[]>` — GET /api/v1/plans/playbook
|
||||||
|
- `uploadBackup(blob: Buffer, metadata: BackupMetadata): Promise<void>` — PUT /api/v1/backup
|
||||||
|
- `downloadBackup(): Promise<{ blob: Buffer, metadata: BackupMetadata }>` — GET /api/v1/backup
|
||||||
|
- Automatic retry with exponential backoff (max 3 attempts)
|
||||||
|
- Offline detection: returns cached playbook responses when offline
|
||||||
|
- `isOnline(): boolean` — connectivity check
|
||||||
|
- [ ] Create `src/main/api/plan-runner.ts`:
|
||||||
|
- `PlanRunner` class:
|
||||||
|
- `execute(plan: ExecutionPlan): Promise<PlanResult>` — executes plan steps locally
|
||||||
|
- Step handlers: `create_record` (inserts into SQLite), `update_record`, `delete_record`, `index_document` (upserts into vector store), `send_notification` (Electron notification API)
|
||||||
|
- Each step logs to activity log
|
||||||
|
- Supports `data_from_step` references (pipeline execution)
|
||||||
|
- Validates plan structure before execution
|
||||||
|
- **Files:** `src/main/api/backend-client.ts`, `src/main/api/plan-runner.ts`
|
||||||
|
- **Outcome:** Electron can communicate with the cloud backend and execute returned plans locally.
|
||||||
|
|
||||||
|
### Step 3.2 — Refactor orchestrator to delegate to backend
|
||||||
|
- [ ] Update `src/main/ai/orchestrator.ts`:
|
||||||
|
- When online: forward chat requests to backend via `BackendClient.chatStream()`
|
||||||
|
- Build `ChatRequest` from local context: query SQLite for user profile, relevant documents (from vector store), recent tasks, conversation history
|
||||||
|
- Stream backend response tokens to renderer via existing `ai:stream` IPC channel
|
||||||
|
- Execute any returned actions via `PlanRunner`
|
||||||
|
- When offline: fall back to local orchestration (existing LangGraph pipeline) with degraded capabilities
|
||||||
|
- Remove direct agent logic (project agent, knowledge agent, general agent tool definitions) — these now live on the backend
|
||||||
|
- Keep `buildProjectContext()` and `buildGlobalContext()` as context builders for the request payload
|
||||||
|
- [ ] Update `src/main/router/index.ts` `ai` sub-router:
|
||||||
|
- `chat` mutation: call refactored orchestrator (which now delegates to backend)
|
||||||
|
- Add `getPlaybooks` query: fetches cached playbooks
|
||||||
|
- Keep `dailyBrief` mutation: sends daily brief request to backend
|
||||||
|
- [ ] Add IPC handler for plan execution results
|
||||||
|
- **Files:** `src/main/ai/orchestrator.ts`, `src/main/router/index.ts`, `src/main/ipc.ts`
|
||||||
|
- **Outcome:** Chat intelligence lives on the backend; Electron is the execution layer.
|
||||||
|
|
||||||
|
### Step 3.3 — Implement Shared Memory (three-tier local memory)
|
||||||
|
- [ ] Create `src/main/database/shared-memory.ts`:
|
||||||
|
- **Short-term memory**: In-memory conversation buffer
|
||||||
|
- `ConversationBuffer` class: stores last N messages per session
|
||||||
|
- `addMessage(sessionId, role, content)`, `getHistory(sessionId, limit?) -> Message[]`
|
||||||
|
- Cleared on session end
|
||||||
|
- **Long-term KV store**: SQLite-backed key-value store
|
||||||
|
- New `agent_memory` table: `id`, `namespace` (agent name), `key`, `value` (JSON text), `updated_at`
|
||||||
|
- `AgentMemoryStore` class: `get(namespace, key)`, `set(namespace, key, value)`, `delete(namespace, key)`, `listKeys(namespace)`
|
||||||
|
- Used by agents to persist learned facts, user preferences
|
||||||
|
- **Vector store**: Already exists (LanceDB). Enhance with:
|
||||||
|
- Multi-collection support: separate tables for notes, emails, files, calendar
|
||||||
|
- `searchByCollection(collection, query, limit) -> SearchResult[]`
|
||||||
|
- [ ] Add `agent_memory` table to `src/main/db/schema.ts`
|
||||||
|
- [ ] Generate migration
|
||||||
|
- **Files:** `src/main/database/shared-memory.ts`, `src/main/db/schema.ts`, `src/main/db/migrations/`
|
||||||
|
- **Outcome:** Three-tier memory system supporting short-term conversation, long-term agent facts, and semantic search.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4 — Security: E2E Backup & Offline Mode
|
||||||
|
|
||||||
|
### Step 4.1 — Implement E2E encrypted backup
|
||||||
|
- [ ] Create `src/main/backup/e2e-crypto.ts`:
|
||||||
|
- `generatePassphrase(): string` — BIP39-compatible 12-word recovery phrase
|
||||||
|
- `deriveKey(passphrase: string, salt: Buffer): Promise<Buffer>` — Argon2id key derivation (time cost 3, memory 64MB, parallelism 1)
|
||||||
|
- `encrypt(data: Buffer, key: Buffer): { ciphertext: Buffer, iv: Buffer, authTag: Buffer }` — AES-256-GCM
|
||||||
|
- `decrypt(ciphertext: Buffer, key: Buffer, iv: Buffer, authTag: Buffer): Buffer`
|
||||||
|
- Uses `node:crypto` for AES and `argon2` npm package for key derivation
|
||||||
|
- [ ] Create `src/main/backup/backup-manager.ts`:
|
||||||
|
- `BackupManager` class:
|
||||||
|
- `createBackup(passphrase: string): Promise<BackupBlob>` — Exports SQLite DB, encrypts, returns blob + metadata
|
||||||
|
- `restoreBackup(blob: Buffer, passphrase: string): Promise<void>` — Decrypts blob, replaces local DB, re-initializes
|
||||||
|
- `uploadBackup(passphrase: string): Promise<void>` — Creates backup, uploads via `BackendClient`
|
||||||
|
- `downloadAndRestore(passphrase: string): Promise<void>` — Downloads from backend, decrypts, restores
|
||||||
|
- Incremental backup: chunks DB into segments, encrypts each separately, tracks content hashes to skip unchanged chunks
|
||||||
|
- Metadata header: version, timestamp, checksum (SHA-256 of plaintext), chunk count
|
||||||
|
- **Files:** `src/main/backup/e2e-crypto.ts`, `src/main/backup/backup-manager.ts`
|
||||||
|
- **Outcome:** User data never leaves the device unencrypted. Backend stores only opaque blobs.
|
||||||
|
|
||||||
|
### Step 4.2 — Implement offline sync queue
|
||||||
|
- [ ] Create `src/main/backup/sync-queue.ts`:
|
||||||
|
- `SyncQueue` class:
|
||||||
|
- `enqueue(action: QueuedAction): void` — Adds action to persistent queue (SQLite table `sync_queue`)
|
||||||
|
- `processQueue(): Promise<void>` — Processes queued actions in FIFO order when online
|
||||||
|
- `getQueueSize(): number`
|
||||||
|
- `clearQueue(): void`
|
||||||
|
- Conflict resolution: last-write-wins with timestamps
|
||||||
|
- New `sync_queue` table: `id`, `action_type`, `payload` (JSON), `created_at`, `status` (pending/processing/failed), `retry_count`, `last_error`
|
||||||
|
- Auto-drain: watches connectivity, starts processing when online
|
||||||
|
- Failed actions: retry up to 3 times with exponential backoff, then mark as `failed` for user review
|
||||||
|
- [ ] Add `sync_queue` table to schema
|
||||||
|
- [ ] Integrate with `BackendClient`: when offline, chat/backup calls enqueue instead of failing
|
||||||
|
- **Files:** `src/main/backup/sync-queue.ts`, `src/main/db/schema.ts`, `src/main/api/backend-client.ts`
|
||||||
|
- **Outcome:** App works offline; queued actions sync automatically when connectivity returns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5 — Auth Integration & Database Encryption
|
||||||
|
|
||||||
|
### Step 5.1 — Integrate auth into Electron app
|
||||||
|
- [ ] Create `src/main/auth/auth-manager.ts`:
|
||||||
|
- `AuthManager` class:
|
||||||
|
- `login(email, password): Promise<void>` — Calls backend POST /api/v1/auth/login, stores JWT in secure storage (via token.ts)
|
||||||
|
- `register(email, password): Promise<void>` — Calls POST /api/v1/auth/register
|
||||||
|
- `logout(): void` — Clears stored JWT
|
||||||
|
- `getToken(): string | null` — Returns current JWT
|
||||||
|
- `refreshToken(): Promise<void>` — Auto-refresh before expiry
|
||||||
|
- `isAuthenticated(): boolean`
|
||||||
|
- `getCurrentTier(): BillingTier`
|
||||||
|
- Auto-refresh: checks token expiry every 5 minutes, refreshes if < 10 minutes remaining
|
||||||
|
- [ ] Add tRPC procedures: `auth.login`, `auth.register`, `auth.logout`, `auth.status`, `auth.tier`
|
||||||
|
- [ ] Wire `BackendClient` to use `AuthManager.getToken()` for all requests
|
||||||
|
- **Files:** `src/main/auth/auth-manager.ts`, `src/main/router/index.ts`, `src/main/api/backend-client.ts`
|
||||||
|
- **Outcome:** Electron app has full auth flow; backend requests are authenticated.
|
||||||
|
|
||||||
|
### Step 5.2 — Migrate from better-sqlite3 to SQLCipher
|
||||||
|
- [ ] Add `@journeyapps/sqlcipher` to dependencies (replaces `better-sqlite3`)
|
||||||
|
- [ ] Update `src/main/db/index.ts`:
|
||||||
|
- Replace `better-sqlite3` import with `@journeyapps/sqlcipher`
|
||||||
|
- On first launch: derive DB key from OS keychain or prompt user
|
||||||
|
- `initDb(password)`: opens DB with `PRAGMA key = 'password'`
|
||||||
|
- Migration path for existing unencrypted DBs: detect → export → create encrypted → import → delete old
|
||||||
|
- WAL mode still enabled after keying
|
||||||
|
- [ ] Update `src/main/index.ts`: pass password to `initDb()`
|
||||||
|
- [ ] Test that all existing Drizzle operations work with SQLCipher
|
||||||
|
- **Files:** `package.json`, `src/main/db/index.ts`, `src/main/index.ts`
|
||||||
|
- **Outcome:** All local data encrypted at rest with SQLCipher.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6 — Renderer UI Updates
|
||||||
|
|
||||||
|
### Step 6.1 — Update Settings page for multi-provider config
|
||||||
|
- [ ] Add provider management UI to Settings:
|
||||||
|
- List of configured providers with status (active/inactive/error)
|
||||||
|
- Add provider form: name dropdown (OpenAI, Anthropic, Google, Mistral, Groq, Ollama), API key input, model selection, endpoint (for Ollama)
|
||||||
|
- Set primary and fallback providers
|
||||||
|
- Test connection button per provider
|
||||||
|
- [ ] Add auth section to Settings:
|
||||||
|
- Login/register form
|
||||||
|
- Current tier display with upgrade CTA
|
||||||
|
- Logout button
|
||||||
|
- [ ] Add backup section to Settings:
|
||||||
|
- Create/view recovery passphrase
|
||||||
|
- Manual backup trigger
|
||||||
|
- Backup history with restore points
|
||||||
|
- Auto-backup schedule toggle
|
||||||
|
- **Files:** `src/renderer/components/settings/` (new), route file
|
||||||
|
- **Outcome:** Users can manage AI providers, auth, and backups from Settings.
|
||||||
|
|
||||||
|
### Step 6.2 — Add Permission Dialog and Activity Log
|
||||||
|
- [ ] Create `src/renderer/components/permissions/PermissionDialog.tsx`:
|
||||||
|
- Modal shown when a plugin requests new permissions
|
||||||
|
- Lists requested permissions with reasons
|
||||||
|
- Per-permission approve/deny toggles
|
||||||
|
- Shows plugin manifest info (name, description, version)
|
||||||
|
- [ ] Create `src/renderer/components/permissions/ActivityLog.tsx`:
|
||||||
|
- Filterable table of all plugin activity
|
||||||
|
- Columns: timestamp, plugin name, action type, resource, status
|
||||||
|
- Filter by plugin, date range, action type
|
||||||
|
- Export as CSV
|
||||||
|
- [ ] Add tRPC procedures for permission management and activity log queries
|
||||||
|
- **Files:** `src/renderer/components/permissions/PermissionDialog.tsx`, `src/renderer/components/permissions/ActivityLog.tsx`, `src/main/router/index.ts`
|
||||||
|
- **Outcome:** Transparent permission system with full activity audit trail.
|
||||||
|
|
||||||
|
### Step 6.3 — Update AIChatPanel for backend-powered chat
|
||||||
|
- [ ] Update `src/renderer/hooks/useAIChat.ts`:
|
||||||
|
- Support WebSocket streaming from backend (when online)
|
||||||
|
- Fall back to IPC streaming (when offline, using local orchestrator)
|
||||||
|
- Add connection status indicator (online/offline/reconnecting)
|
||||||
|
- Support execution plan responses: show plan preview, allow user to approve/modify before execution
|
||||||
|
- [ ] Update `src/renderer/components/ai/AIChatPanel.tsx`:
|
||||||
|
- Add connection status badge
|
||||||
|
- Add tier indicator (shows current plan limitations)
|
||||||
|
- Plan approval UI: expandable plan steps with approve/reject buttons
|
||||||
|
- Enhanced error states: differentiate between offline, auth expired, rate limited, server error
|
||||||
|
- [ ] Update `src/renderer/components/ai/FloatingChat.tsx`:
|
||||||
|
- Same streaming changes as AIChatPanel
|
||||||
|
- Compact plan approval for inline context
|
||||||
|
- **Files:** `src/renderer/hooks/useAIChat.ts`, `src/renderer/components/ai/AIChatPanel.tsx`, `src/renderer/components/ai/FloatingChat.tsx`
|
||||||
|
- **Outcome:** Chat UI seamlessly handles both online (backend) and offline (local) modes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7 — Cleanup & Hardening
|
||||||
|
|
||||||
|
### Step 7.1 — Remove deprecated AI code
|
||||||
|
- [ ] Delete `src/main/ai/copilot.ts` (Copilot SDK replaced by LiteLLM)
|
||||||
|
- [ ] Delete `src/main/ai/chat-copilot.ts` (LangChain adapter no longer needed)
|
||||||
|
- [ ] Delete or archive `src/main/ai/llm.ts` (replaced by `src/main/llm/litellm-client.ts`)
|
||||||
|
- [ ] Remove `@github/copilot-sdk`, `@langchain/langgraph` from dependencies (if unused)
|
||||||
|
- [ ] Clean up `src/main/ai/provider.ts`: simplify to delegate to `src/main/llm/providers.ts`
|
||||||
|
- [ ] Remove `currentSender` module-level mutable state from orchestrator (proper context passing)
|
||||||
|
- [ ] Update `src/main/index.ts` startup: remove `import './ai/copilot'`, add `BatchRunner.startScheduler()`, add `AuthManager` init
|
||||||
|
- **Files:** Multiple files under `src/main/ai/`, `package.json`, `src/main/index.ts`
|
||||||
|
- **Outcome:** No dead code; clean, maintainable codebase.
|
||||||
|
|
||||||
|
### Step 7.2 — Add error handling and logging
|
||||||
|
- [ ] Implement structured logging in main process:
|
||||||
|
- Log levels: debug, info, warn, error
|
||||||
|
- Log destinations: console (dev), file (production, rotated)
|
||||||
|
- Correlation IDs for request tracing across IPC → backend → response
|
||||||
|
- [ ] Add error boundaries in renderer:
|
||||||
|
- Per-route error boundaries
|
||||||
|
- AI chat error boundary (graceful degradation)
|
||||||
|
- Plugin error boundary (shows which plugin failed)
|
||||||
|
- **Files:** `src/main/utils/logger.ts` (new), `src/renderer/components/ErrorBoundary.tsx` (new)
|
||||||
|
- **Outcome:** Production-ready error handling and observability.
|
||||||
|
|
||||||
|
### Step 7.3 — Electron integration tests
|
||||||
|
- [ ] Test BackendClient with mocked HTTP responses
|
||||||
|
- [ ] Test PlanRunner with sample execution plans
|
||||||
|
- [ ] Test SyncQueue offline → online transition
|
||||||
|
- [ ] Test BackupManager encrypt → decrypt round-trip
|
||||||
|
- [ ] Test PermissionManager grant → check → revoke cycle
|
||||||
|
- **Files:** `src/main/__tests__/` (new test directory)
|
||||||
|
- **Outcome:** Confidence that all Electron-side components work correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Dependencies (package.json)
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `@journeyapps/sqlcipher` | Encrypted SQLite (replaces `better-sqlite3`) |
|
||||||
|
| `argon2` | Key derivation for E2E backup |
|
||||||
|
| `node-cron` | Batch agent scheduling |
|
||||||
|
| `chokidar` | File watching (FileWatcher plugin) |
|
||||||
|
| `imapflow` | IMAP client (EmailScanner plugin) |
|
||||||
|
| `onnxruntime-node` | Local embeddings (optional) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Notes
|
||||||
|
|
||||||
|
- **Each step is independently committable** and produces working code.
|
||||||
|
- **Phases 1-2** (LLM client + plugins) are independent of the backend — can start immediately.
|
||||||
|
- **Phase 3** (backend integration) requires the backend repo to have the `/api/v1/chat` endpoint ready.
|
||||||
|
- **Phase 5.2** (SQLCipher) is intentionally late to avoid encryption overhead during active schema changes.
|
||||||
|
- **The existing app continues to work** throughout the migration. Local orchestration is preserved until backend is ready (Step 3.2).
|
||||||
358
BACKEND_PLAN.md
Normal file
358
BACKEND_PLAN.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# Backend Plan — Adiuva Cloud API
|
||||||
|
|
||||||
|
> **Separate repository.** This document defines the FastAPI backend that the Electron app communicates with.
|
||||||
|
>
|
||||||
|
> The backend owns: orchestration logic, chat agent intelligence, prompt IP, auth, billing, and backup blob storage.
|
||||||
|
> The backend NEVER persists user data. It receives context in requests, uses it for orchestration, and discards it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
adiuva-backend/
|
||||||
|
├── app/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── main.py # FastAPI entry + CORS + lifespan + router includes
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── agent_registry.py # Base classes + singleton registry
|
||||||
|
│ │ ├── orchestrator.py # LLM-based intent router
|
||||||
|
│ │ ├── execution_plan.py # Plan builder + cache
|
||||||
|
│ │ └── plugin_loader.py # Dynamic agent loading
|
||||||
|
│ ├── agents/
|
||||||
|
│ │ ├── __init__.py # Auto-registers all agents
|
||||||
|
│ │ ├── task_agent.py
|
||||||
|
│ │ ├── calendar_agent.py
|
||||||
|
│ │ ├── email_agent.py
|
||||||
|
│ │ └── analytics_agent.py
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── routes/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ ├── chat.py # POST /chat + WS /chat/stream
|
||||||
|
│ │ │ ├── plans.py # GET /plans/playbook
|
||||||
|
│ │ │ ├── backup.py # PUT/GET /backup
|
||||||
|
│ │ │ ├── auth.py # Register/login/refresh
|
||||||
|
│ │ │ └── billing.py # Checkout/webhook/subscription
|
||||||
|
│ │ └── middleware/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── auth.py # JWT validation
|
||||||
|
│ │ ├── rate_limit.py # Tier-aware rate limiting
|
||||||
|
│ │ └── sanitizer.py # Strip prompt metadata from responses
|
||||||
|
│ ├── billing/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── stripe_service.py # Stripe checkout + webhooks
|
||||||
|
│ │ └── tier_manager.py # Feature matrix per tier
|
||||||
|
│ └── config/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── settings.py # Pydantic BaseSettings (env-based)
|
||||||
|
├── tests/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── conftest.py # Fixtures: test client, mock agents, mock LLM
|
||||||
|
│ ├── test_orchestrator.py
|
||||||
|
│ ├── test_agents.py
|
||||||
|
│ ├── test_auth.py
|
||||||
|
│ └── test_backup.py
|
||||||
|
├── alembic/ # DB migrations (auth/billing tables only)
|
||||||
|
│ ├── alembic.ini
|
||||||
|
│ └── versions/
|
||||||
|
├── requirements.txt
|
||||||
|
├── Dockerfile
|
||||||
|
├── docker-compose.yml # App + PostgreSQL + Redis (dev)
|
||||||
|
├── .env.example
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Implementation
|
||||||
|
|
||||||
|
### Step 1 — Project scaffolding
|
||||||
|
- [ ] Initialize repo with the directory structure above
|
||||||
|
- [ ] Write `requirements.txt`:
|
||||||
|
```
|
||||||
|
fastapi>=0.115.0
|
||||||
|
uvicorn[standard]>=0.34.0
|
||||||
|
langchain>=0.3.0
|
||||||
|
langchain-openai>=0.3.0
|
||||||
|
pydantic>=2.10.0
|
||||||
|
python-jose[cryptography]>=3.3.0
|
||||||
|
stripe>=11.0.0
|
||||||
|
boto3>=1.35.0
|
||||||
|
slowapi>=0.1.9
|
||||||
|
sqlalchemy>=2.0.0
|
||||||
|
asyncpg>=0.30.0
|
||||||
|
alembic>=1.14.0
|
||||||
|
bcrypt>=4.2.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
httpx>=0.28.0
|
||||||
|
websockets>=14.0
|
||||||
|
pytest>=8.0.0
|
||||||
|
pytest-asyncio>=0.24.0
|
||||||
|
```
|
||||||
|
- [ ] Write `app/main.py`: FastAPI app with CORS (allow `app://`, `http://localhost:*`), lifespan (init DB pool, init agent registry), include all routers under `/api/v1`
|
||||||
|
- [ ] Write `app/config/settings.py`: `Settings(BaseSettings)` with fields: `DATABASE_URL`, `JWT_SECRET`, `JWT_ALGORITHM` (default HS256), `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `S3_BUCKET`, `S3_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `OPENAI_API_KEY`, `CORS_ORIGINS`, `ENV` (dev/prod)
|
||||||
|
- [ ] Write `Dockerfile`: Python 3.12 slim, multi-stage (builder + runtime), non-root user
|
||||||
|
- [ ] Write `docker-compose.yml`: app, postgres:16, optional redis
|
||||||
|
- [ ] Write `.env.example`
|
||||||
|
- **Outcome:** Runnable FastAPI skeleton (returns 404 on all routes).
|
||||||
|
|
||||||
|
### Step 2 — Pydantic schemas (API contracts)
|
||||||
|
- [ ] Create `app/schemas.py` (mirrors `src/shared/api-types.ts` from Electron repo):
|
||||||
|
- `ChatRequest`: `message: str`, `context: ChatContext`, `execution_mode: Literal['direct', 'plan']`
|
||||||
|
- `ChatContext`: `user_profile: dict`, `relevant_documents: list[str]`, `recent_tasks: list[dict]`, `conversation_history: list[dict]`
|
||||||
|
- `ChatResponse`: `response: str`, `actions: list[PlanAction]`
|
||||||
|
- `PlanAction`: `type: Literal['create_record', 'update_record', 'delete_record', 'index_document', 'send_notification']`, `table: str | None`, `data: dict | None`
|
||||||
|
- `ExecutionPlan`: `agent: str`, `steps: list[PlanStep]`
|
||||||
|
- `PlanStep`: `action: str`, `prompt_template: str | None`, `variables: dict | None`, `data_from_step: int | None`
|
||||||
|
- `BackupMetadata`: `version: int`, `timestamp: int`, `checksum: str`, `chunk_count: int`
|
||||||
|
- `BillingTier`: `Literal['free', 'pro', 'power', 'team']`
|
||||||
|
- `AuthTokens`: `access_token: str`, `refresh_token: str`, `expires_at: int`
|
||||||
|
- `UserProfile`: `id: str`, `email: str`, `tier: BillingTier`
|
||||||
|
- **Outcome:** All request/response models defined and validated.
|
||||||
|
|
||||||
|
### Step 3 — Agent Registry + base classes
|
||||||
|
- [ ] `app/core/agent_registry.py`:
|
||||||
|
- `BaseAgent(ABC)`:
|
||||||
|
- `user_id: str`, `shared_memory: dict`, `vector_store_context: list[str]`, `skills: list[str]`
|
||||||
|
- Abstract `get_name() -> str`, `get_description() -> str`
|
||||||
|
- `ChatAgent(BaseAgent)`:
|
||||||
|
- Abstract `async handle(query: str, context: dict) -> str`
|
||||||
|
- Abstract `get_tools() -> list` (LangChain tool definitions)
|
||||||
|
- Concrete `_tool_loop(llm, messages, tools, max_iter=5) -> str` — shared tool-calling loop
|
||||||
|
- `AgentRegistry` (singleton):
|
||||||
|
- `_agents: dict[str, ChatAgent]`
|
||||||
|
- `register(agent_class)` — decorator pattern
|
||||||
|
- `get(name) -> ChatAgent`
|
||||||
|
- `list_agents() -> list[dict]` — returns `[{name, description}]` for orchestrator prompt
|
||||||
|
- `async call_agent(name, query, context) -> str` — for inter-agent calls
|
||||||
|
- [ ] Unit tests: register, get, list, call_agent with mock
|
||||||
|
- **Outcome:** Pluggable agent framework.
|
||||||
|
|
||||||
|
### Step 4 — Orchestrator
|
||||||
|
- [ ] `app/core/orchestrator.py`:
|
||||||
|
- `async classify_intent(message, context, registry) -> str`:
|
||||||
|
- System prompt: "You are an intent classifier. Given the user message and context, decide which agent to route to. Available agents: {registry.list_agents()}. Respond with just the agent name."
|
||||||
|
- Uses gpt-4o-mini via LangChain for low latency
|
||||||
|
- Falls back to `task_agent` if no clear match
|
||||||
|
- `async route_single(agent_name, message, context) -> ChatResponse`:
|
||||||
|
- Instantiates agent from registry
|
||||||
|
- Calls `agent.handle(message, context)`
|
||||||
|
- Returns response + any actions the agent produced
|
||||||
|
- `async route_pipeline(agent_names, message, context) -> ChatResponse`:
|
||||||
|
- Executes agents in sequence
|
||||||
|
- Each agent receives `{...context, previous_results: [...]}`
|
||||||
|
- Final synthesis via LLM: "Summarize these agent results into a coherent response"
|
||||||
|
- `async orchestrate(request: ChatRequest) -> ChatResponse | ExecutionPlan`:
|
||||||
|
- Main entry point
|
||||||
|
- Classifies intent
|
||||||
|
- If `execution_mode == 'direct'`: route + return response
|
||||||
|
- If `execution_mode == 'plan'`: route + return execution plan with template IDs
|
||||||
|
- `async orchestrate_stream(request: ChatRequest) -> AsyncGenerator[str, None]`:
|
||||||
|
- Same as orchestrate but yields tokens for WebSocket streaming
|
||||||
|
- [ ] Integration tests with mocked LLM and mocked agents
|
||||||
|
- **Outcome:** Intelligent routing with single-agent and pipeline modes.
|
||||||
|
|
||||||
|
### Step 5 — Execution Plan generator
|
||||||
|
- [ ] `app/core/execution_plan.py`:
|
||||||
|
- `PromptTemplateRegistry`: dict of `template_id -> prompt_text`. Templates are server-side only — client receives IDs.
|
||||||
|
- `ExecutionPlanBuilder`:
|
||||||
|
- `add_step(action, params) -> self`
|
||||||
|
- `add_llm_step(template_id, variables) -> self`
|
||||||
|
- `add_data_step(action, data_from_step) -> self`
|
||||||
|
- `build() -> ExecutionPlan` — validates step references
|
||||||
|
- `PlanCache`:
|
||||||
|
- In-memory LRU (maxsize=1000)
|
||||||
|
- `cache_plan(key, plan)`, `get_plan(key)`, `get_all_playbooks() -> list[ExecutionPlan]`
|
||||||
|
- Playbooks are pre-built plans for common operations (e.g., "create task from email", "generate weekly report")
|
||||||
|
- **Outcome:** Plans are cacheable as playbooks. Prompt IP never leaves the server.
|
||||||
|
|
||||||
|
### Step 6 — Chat Agents
|
||||||
|
- [ ] `app/agents/task_agent.py` — `@registry.register`:
|
||||||
|
- Description: "Manages tasks: create, update, list, suggest"
|
||||||
|
- Tools: `create_task(title, description, priority, due_date)`, `update_task(id, updates)`, `list_tasks(filters)`, `suggest_tasks(notes_context)`
|
||||||
|
- System prompt: PM-oriented, validates task structure, infers priority from context
|
||||||
|
- `handle()`: LLM + tool loop via `_tool_loop()`, returns response text + list of actions performed
|
||||||
|
- [ ] `app/agents/calendar_agent.py` — `@registry.register`:
|
||||||
|
- Description: "Calendar management: events, conflicts, scheduling"
|
||||||
|
- Tools: `list_events(date_range)`, `detect_conflicts(events)`, `suggest_reschedule(conflict)`
|
||||||
|
- Works with event metadata passed in context (never raw calendar data stored)
|
||||||
|
- [ ] `app/agents/email_agent.py` — `@registry.register`:
|
||||||
|
- Description: "Email analysis: classify, extract actions, draft responses"
|
||||||
|
- Tools: `classify_email(metadata)`, `extract_action_items(metadata)`, `draft_response(thread_context)`
|
||||||
|
- Only processes metadata sent by client — never raw email bodies
|
||||||
|
- [ ] `app/agents/analytics_agent.py` — `@registry.register`:
|
||||||
|
- Description: "Workspace analytics: metrics, reports, trends"
|
||||||
|
- Tools: `calculate_metrics(task_data)`, `generate_report(period, data)`, `trend_analysis(data_points)`
|
||||||
|
- Crunches numbers from context, returns structured insights
|
||||||
|
- [ ] `app/agents/__init__.py`: imports all agent modules to trigger `@registry.register` decorators
|
||||||
|
- [ ] Unit tests per agent with mocked LLM
|
||||||
|
- **Outcome:** Four specialized agents, all registered and tested.
|
||||||
|
|
||||||
|
### Step 7 — API Routes
|
||||||
|
|
||||||
|
#### 7a — Chat endpoint
|
||||||
|
- [ ] `app/api/routes/chat.py`:
|
||||||
|
- `POST /api/v1/chat`:
|
||||||
|
- Request: `ChatRequest`
|
||||||
|
- Calls `orchestrate(request)` or `orchestrate()` + `build_plan()`
|
||||||
|
- Response: `ChatResponse` or `ExecutionPlan`
|
||||||
|
- `WebSocket /api/v1/chat/stream`:
|
||||||
|
- Client sends `ChatRequest` as first JSON frame
|
||||||
|
- Server yields token strings via `orchestrate_stream()`
|
||||||
|
- Final frame: JSON `ChatResponse` with `{"done": true, "response": "...", "actions": [...]}`
|
||||||
|
- Heartbeat ping every 30s to keep connection alive
|
||||||
|
|
||||||
|
#### 7b — Plans endpoint
|
||||||
|
- [ ] `app/api/routes/plans.py`:
|
||||||
|
- `GET /api/v1/plans/playbook`: Returns all playbooks available for the user's tier
|
||||||
|
- `GET /api/v1/plans/playbook/{plan_id}`: Returns a specific plan
|
||||||
|
|
||||||
|
#### 7c — Backup endpoint
|
||||||
|
- [ ] `app/api/routes/backup.py`:
|
||||||
|
- `PUT /api/v1/backup`: Accepts binary blob + metadata headers (`X-Backup-Version`, `X-Backup-Timestamp`, `X-Backup-Checksum`). Stores in S3 keyed by `{user_id}/{timestamp}`. Enforces tier limits:
|
||||||
|
- Free: 0 (no backup)
|
||||||
|
- Pro: 5 GB
|
||||||
|
- Power: 50 GB
|
||||||
|
- Team: unlimited
|
||||||
|
- `GET /api/v1/backup`: Returns latest blob for authenticated user. Supports `If-Modified-Since`.
|
||||||
|
- `GET /api/v1/backup/history`: Returns list of `BackupMetadata` (no blobs).
|
||||||
|
- `DELETE /api/v1/backup/{backup_id}`: Delete specific backup.
|
||||||
|
|
||||||
|
#### 7d — Auth endpoint
|
||||||
|
- [ ] `app/api/routes/auth.py`:
|
||||||
|
- `POST /api/v1/auth/register`: `{email, password}` → bcrypt hash → insert user → return `AuthTokens`
|
||||||
|
- `POST /api/v1/auth/login`: Validate credentials → return `AuthTokens`
|
||||||
|
- `POST /api/v1/auth/refresh`: Rotate refresh token → return new `AuthTokens`
|
||||||
|
- `GET /api/v1/auth/me`: Return `UserProfile` for current JWT
|
||||||
|
|
||||||
|
#### 7e — Billing endpoint
|
||||||
|
- [ ] `app/api/routes/billing.py`:
|
||||||
|
- `POST /api/v1/billing/checkout`: Creates Stripe checkout session → returns URL
|
||||||
|
- `POST /api/v1/billing/webhook`: Handles Stripe webhooks (subscription lifecycle)
|
||||||
|
- `GET /api/v1/billing/subscription`: Returns current subscription info
|
||||||
|
- `DELETE /api/v1/billing/subscription`: Cancels subscription
|
||||||
|
|
||||||
|
- **Outcome:** Complete REST + WebSocket API.
|
||||||
|
|
||||||
|
### Step 8 — Middleware
|
||||||
|
|
||||||
|
#### 8a — Auth middleware
|
||||||
|
- [ ] `app/api/middleware/auth.py`:
|
||||||
|
- FastAPI dependency: `get_current_user(token: str = Depends(oauth2_scheme)) -> UserProfile`
|
||||||
|
- Validates JWT signature, expiry, extracts `user_id` and `tier`
|
||||||
|
- Raises `401` on invalid/expired token
|
||||||
|
- Exempt routes: `/api/v1/auth/register`, `/api/v1/auth/login`, `/api/v1/billing/webhook`
|
||||||
|
|
||||||
|
#### 8b — Rate limiter
|
||||||
|
- [ ] `app/api/middleware/rate_limit.py`:
|
||||||
|
- Uses `slowapi` with `Limiter(key_func=get_user_id_from_jwt)`
|
||||||
|
- Tier-based limits:
|
||||||
|
- Free: 20 req/min
|
||||||
|
- Pro: 60 req/min
|
||||||
|
- Power: 120 req/min
|
||||||
|
- Team: 200 req/seat/min
|
||||||
|
- Custom 429 response with `Retry-After` header
|
||||||
|
|
||||||
|
#### 8c — Sanitizer
|
||||||
|
- [ ] `app/api/middleware/sanitizer.py`:
|
||||||
|
- Response middleware that scans response bodies
|
||||||
|
- Strips: system prompt fragments, agent internal reasoning, tool schemas, routing metadata
|
||||||
|
- Pattern-based detection + exact match against known prompt fingerprints
|
||||||
|
- Logs sanitization events for monitoring
|
||||||
|
|
||||||
|
- **Outcome:** Secure, rate-limited API with prompt IP protection.
|
||||||
|
|
||||||
|
### Step 9 — Billing & Tier management
|
||||||
|
- [ ] `app/billing/stripe_service.py`:
|
||||||
|
- `create_checkout_session(user_id, tier) -> str`
|
||||||
|
- `handle_webhook(payload, sig_header) -> None`: processes `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`
|
||||||
|
- `get_subscription(user_id) -> dict | None`
|
||||||
|
- `cancel_subscription(user_id) -> None`
|
||||||
|
- [ ] `app/billing/tier_manager.py`:
|
||||||
|
- `TierManager`:
|
||||||
|
- Feature matrix:
|
||||||
|
```python
|
||||||
|
FEATURES = {
|
||||||
|
'free': {'agents': 3, 'batch': False, 'providers': 1, 'backup_gb': 0},
|
||||||
|
'pro': {'agents': -1, 'batch': True, 'providers': -1, 'backup_gb': 5},
|
||||||
|
'power': {'agents': -1, 'batch': True, 'providers': -1, 'backup_gb': 50, 'byok': True},
|
||||||
|
'team': {'agents': -1, 'batch': True, 'providers': -1, 'backup_gb': -1, 'sso': True},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `get_tier(user_id) -> BillingTier`
|
||||||
|
- `check_feature(user_id, feature) -> bool`
|
||||||
|
- `get_rate_limit(tier) -> int`
|
||||||
|
- **Outcome:** Stripe integration with tier-based feature gating.
|
||||||
|
|
||||||
|
### Step 10 — Database (auth/billing only)
|
||||||
|
- [ ] PostgreSQL schema via Alembic:
|
||||||
|
- `users`: `id UUID PK`, `email UNIQUE`, `password_hash`, `tier` (default 'free'), `stripe_customer_id`, `created_at`, `updated_at`
|
||||||
|
- `refresh_tokens`: `id UUID PK`, `user_id FK`, `token_hash`, `expires_at`, `created_at`
|
||||||
|
- `subscriptions`: `id UUID PK`, `user_id FK`, `stripe_subscription_id`, `tier`, `status`, `current_period_end`, `created_at`
|
||||||
|
- `backup_metadata`: `id UUID PK`, `user_id FK`, `s3_key`, `version`, `timestamp`, `checksum`, `size_bytes`, `created_at`
|
||||||
|
- [ ] Initial Alembic migration
|
||||||
|
- [ ] SQLAlchemy models in `app/models.py`
|
||||||
|
- **Outcome:** Auth and billing persistence. Zero user data stored.
|
||||||
|
|
||||||
|
### Step 11 — Testing & deployment
|
||||||
|
- [ ] `tests/conftest.py`: TestClient fixture, mock LLM fixture (`AsyncMock` returning canned responses), mock agent fixture, test DB (SQLite in-memory for speed)
|
||||||
|
- [ ] `tests/test_orchestrator.py`: classify_intent routing, single agent, pipeline, plan mode
|
||||||
|
- [ ] `tests/test_agents.py`: each agent with mocked tools
|
||||||
|
- [ ] `tests/test_auth.py`: register → login → access protected → refresh → expired token
|
||||||
|
- [ ] `tests/test_backup.py`: upload → download → history → delete, tier limit enforcement
|
||||||
|
- [ ] `Dockerfile` optimized for production (gunicorn + uvicorn workers)
|
||||||
|
- [ ] GitHub Actions CI: lint (ruff), test (pytest), build Docker image
|
||||||
|
- **Outcome:** Fully tested, deployable backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Contract Summary
|
||||||
|
|
||||||
|
| Method | Endpoint | Auth | Request | Response |
|
||||||
|
|--------|----------|------|---------|----------|
|
||||||
|
| POST | `/api/v1/auth/register` | No | `{email, password}` | `AuthTokens` |
|
||||||
|
| POST | `/api/v1/auth/login` | No | `{email, password}` | `AuthTokens` |
|
||||||
|
| POST | `/api/v1/auth/refresh` | No | `{refresh_token}` | `AuthTokens` |
|
||||||
|
| GET | `/api/v1/auth/me` | JWT | — | `UserProfile` |
|
||||||
|
| POST | `/api/v1/chat` | JWT | `ChatRequest` | `ChatResponse \| ExecutionPlan` |
|
||||||
|
| WS | `/api/v1/chat/stream` | JWT | `ChatRequest` (first frame) | Token stream + final JSON |
|
||||||
|
| GET | `/api/v1/plans/playbook` | JWT | — | `ExecutionPlan[]` |
|
||||||
|
| GET | `/api/v1/plans/playbook/:id` | JWT | — | `ExecutionPlan` |
|
||||||
|
| PUT | `/api/v1/backup` | JWT | Binary blob + headers | `{ok: true}` |
|
||||||
|
| GET | `/api/v1/backup` | JWT | — | Binary blob |
|
||||||
|
| GET | `/api/v1/backup/history` | JWT | — | `BackupMetadata[]` |
|
||||||
|
| DELETE | `/api/v1/backup/:id` | JWT | — | `{ok: true}` |
|
||||||
|
| POST | `/api/v1/billing/checkout` | JWT | `{tier}` | `{checkout_url}` |
|
||||||
|
| POST | `/api/v1/billing/webhook` | Stripe sig | Stripe event | `{ok: true}` |
|
||||||
|
| GET | `/api/v1/billing/subscription` | JWT | — | Subscription info |
|
||||||
|
| DELETE | `/api/v1/billing/subscription` | JWT | — | `{ok: true}` |
|
||||||
|
| GET | `/api/v1/health` | No | — | `{status, version}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| Framework | FastAPI + Uvicorn |
|
||||||
|
| LLM | LangChain + langchain-openai |
|
||||||
|
| Auth | PyJWT + bcrypt + OAuth2 |
|
||||||
|
| Billing | stripe-python |
|
||||||
|
| Storage | boto3 (S3) |
|
||||||
|
| Database | PostgreSQL + SQLAlchemy + Alembic |
|
||||||
|
| Rate limiting | slowapi |
|
||||||
|
| Testing | pytest + pytest-asyncio + httpx |
|
||||||
|
| Deployment | Docker → fly.io / Railway / AWS ECS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Rules
|
||||||
|
|
||||||
|
1. **NEVER persist user data.** The DB stores only auth, billing, and backup metadata. User context arrives in requests and is discarded after processing.
|
||||||
|
2. **NEVER expose prompts.** System prompts are composed server-side from fragments. Responses are sanitized before sending.
|
||||||
|
3. **Stateless request handling.** No server-side session state. All context comes from the client + JWT.
|
||||||
|
4. **Type hints everywhere.** All functions have full type annotations.
|
||||||
|
5. **Test every agent.** Each chat agent has unit tests with mocked LLM responses.
|
||||||
|
6. **Structured logging.** JSON logs with request ID correlation.
|
||||||
Reference in New Issue
Block a user