# AI Classification and Insights

## Task 012 — In-app email reply classification (implemented)

Inbound **Business Inbox** replies (`EmailReply`) can be classified without creating Leads yet. Classification results are stored in `ai_classifications` and linked from `email_replies.classificationId`.

**Task 013:** Leads & Pipeline — classification **feeds** lead creation via explicit user/API action (`convertEmailReplyToLead`). AI does **not** auto-create leads on classify.

---

## Provider-agnostic architecture

```
src/server/ai/
  ai-provider.interface.ts      # classifyEmailReply(input)
  ai-provider.factory.ts        # getAIProvider(), getAIStatus()
  providers/
    fallback.provider.ts        # rule-based (default)
    openai.provider.ts          # OpenAI SDK when configured
    gemini.provider.ts          # placeholder → fallback
    anthropic.provider.ts       # placeholder → fallback
  prompts/
    classify-email-reply.prompt.ts
```

| `AI_PROVIDER` env | Runtime behavior |
|-------------------|------------------|
| `fallback` (default) | Rule-based keywords (Arabic + English) |
| `openai` | OpenAI chat completions if `OPENAI_API_KEY` set; else fallback |
| `gemini` | Placeholder — uses fallback until SDK integrated |
| `anthropic` | Placeholder — uses fallback until SDK integrated |

OpenAI failures (timeout, invalid JSON, API error) **log a safe message** and **fall back** to rule-based classification.

---

## Classification result schema

Stored on `AiClassification` and returned by preview/classify APIs:

| Field | Values (enum) |
|-------|----------------|
| `intent` | `interested`, `proposal_request`, `meeting_request`, `more_info_request`, `not_interested`, `wrong_contact`, `out_of_office`, `auto_reply`, `spam`, `unknown` |
| `sentiment` | `positive`, `neutral`, `negative`, `unknown` |
| `leadTemperature` | `hot`, `warm`, `cold`, `unknown` |
| `recommendedStage` | `new_lead`, `contacted`, `interested`, `meeting`, `proposal`, `negotiation`, `lost`, `postponed`, `unknown` |
| `recommendedAction` | `call`, `send_proposal`, `send_more_info`, `schedule_meeting`, `follow_up_later`, `mark_unqualified`, `ignore`, `unknown` |
| `serviceInterest` | optional string |
| `summary` | optional string (Arabic preferred when email is Arabic) |
| `confidence` | 0–1 |
| `rawOutput` | provider-specific debug (server-only) |

Types: `src/types/ai-classification.ts` · normalization: `src/lib/ai-classification-validate.ts`

---

## Fallback rules (no API key)

Keyword/regex matching on subject + body (examples):

- **Interested:** مهتم، نرغب، عرض، اجتماع، interested, proposal, meeting
- **Not interested:** غير مهتم، not interested, no need
- **Out of office:** خارج المكتب، out of office, automatic reply
- **Wrong contact:** ليس من اختصاصي، wrong person, not responsible

Confidence increases with stronger matches; uncertain cases use `unknown` and lower confidence.

---

## OpenAI provider

- Enabled when `AI_PROVIDER=openai` and `OPENAI_API_KEY` is set
- Model: `OPENAI_MODEL` (default `gpt-4o-mini`)
- Prompt requires **strict JSON** with allowed enums only; no invented facts
- Package: `openai` (server-only)

---

## Service and APIs

**Service:** `src/server/services/ai-classification.service.ts`

| Function | Behavior |
|----------|----------|
| `classifyEmailReply(id, userId?)` | Load reply + context → classify → upsert `AiClassification` → update `EmailReply.status` → optional `ai_classified` activity |
| `classifyEmailReplyInput(input)` | Classify without DB write |
| `bulkClassifyEmailReplies({ limit })` | Latest unclassified (`status: new`, no `classificationId`); default limit 20; continues on errors |

**APIs** (auth required): see [03-api-specification.md](./03-api-specification.md) — `/api/ai/*`

**UI:** Inbox detail AI panel, inbox bulk classify, `/ai/status` test page.

---

## Email reply status after classification

| Condition | `EmailReply.status` |
|-----------|---------------------|
| Default success | `classified` |
| `intent: spam` | `spam` |
| `not_interested` + `ignore` action | `ignored` |

Task 012 does not create leads. Task 013 adds inbox/API conversion using classification mapping (`src/lib/lead-rules.ts`).

---

## Dashboard metrics (Task 014)

On `GET /api/dashboard` → `ai` and `inbox`:

- Inbox reply counts by status (`new`, `classified`, `linked_to_lead`, etc.)
- Classification totals; positive/negative sentiment; hot/warm/cold `leadTemperature`
- `topIntents` — most common `intent` values on `email_reply` classifications

## Data privacy

- Do not send secrets, credentials, or unrelated PII in prompts
- Truncate very long bodies in prompts when needed
- API keys live in env only — never in client bundles
- See [11-security.md](./11-security.md)

---

## Future (not Task 012)

| Use case | Task |
|----------|------|
| Lead creation from classification | Task 013 (done) — manual/API convert |
| n8n webhooks on classify | Task 019 |
| Market import external GPT prep | task-005 |
| Leadership insight summaries | task-021 |

## Related docs

- [05-email-system.md](./05-email-system.md)
- [10-environment-variables.md](./10-environment-variables.md)
- [01-architecture.md](./01-architecture.md)
