Files
moreminimore-chat/HANDOFF.md
Moreminimore fb55e08a9f
Some checks failed
Auto-assign PR to Author / auto-assign (pull_request) Has been cancelled
Deploy Check / Check Deployment (pull_request) Has been cancelled
Frontend Lint & Test / test (pull_request) Has been cancelled
Log Lines Percentage Check / log_lines_check (pull_request) Has been cancelled
Run Chatwoot CE spec / lint-backend (pull_request) Has been cancelled
Run Chatwoot CE spec / security-scan (pull_request) Has been cancelled
Run Chatwoot CE spec / lint-frontend (pull_request) Has been cancelled
Run Chatwoot CE spec / frontend-tests (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (pull_request) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (pull_request) Has been cancelled
Run MFA Tests / test (pull_request) Has been cancelled
Run Size Limit Check / test (pull_request) Has been cancelled
Test Docker Build / test-build (linux/amd64, ubuntu-latest) (pull_request) Has been cancelled
Test Docker Build / test-build (linux/arm64, ubuntu-22.04-arm) (pull_request) Has been cancelled
Lint PR / Validate PR title (pull_request_target) Has been cancelled
docs: session engineering log + handoff for M2 + OSS chatbot
2026-08-25 16:09:53 +07:00

149 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF
## ═══════ SESSION 2026-08-22 — Final decision: external n8n (Option 3) ═══════
- User selected Option 3: n8n remains a separate service for the LLM chatbot/workflow path.
- Do not integrate Hermes, embed n8n, or change Chatwoot production code for this decision.
- Keep the existing AgentBot async webhook boundary: Chatwoot → external n8n → configured LLM/external services → Chatwoot API reply.
- The Gateway/Adapter design is deferred; implement it only if later requirements justify centralized tenant isolation, idempotency, DLQ, observability, or provider routing.
- Only this handoff/log decision is updated; production source remains unchanged.
## ═══════ SESSION 2026-08-22 — LLM chatbot runtime architecture discovery ═══════
- Read-only architecture review for `/Users/kunthawat/Gitea/Chatwoot`; no production code changed.
- Chatwoot's existing AgentBot path is already the intended boundary: async signed webhook out, external runtime processes, then external runtime posts an outgoing message through Chatwoot API. `outgoing_url` is not an LLM request/response endpoint.
- Hermes is technically feasible only behind a bridge. Hermes API server is the preferred execution interface; Hermes webhook adapter is not plug-compatible because signature headers and response delivery contract differ.
- Recommended target: `Chatwoot AgentBot → dedicated Hermes bridge → Hermes API → Chatwoot API`; optional external n8n remains for visual workflows, CRM/LINE/ERP side effects, and integrations. Do not embed n8n or give a customer-facing Hermes profile unrestricted terminal/file tools.
- Tenant rules: keep Chatwoot as transcript/source of truth; namespace every request by account/inbox/conversation; do not treat Hermes personal memory/profile files as a multi-tenant knowledge base. Use allow-listed tools, sandboxing, idempotency, HMAC verification, secret rotation, and human handoff.
- Next implementation gate: P0 one-account bridge proof (or n8n→Hermes proof), message-created only, no loop, signed webhook verification, reply via bot token, duplicate/retry tests, cross-tenant isolation test, and latency/error telemetry.
- Working tree was already dirty with unrelated M1/M2 changes; future bridge work must use a separate worktree/branch.
## ═══════ SESSION 2026-08-19 — Moreminimore Chat: rebrand + M1 + M2 work ═══════
## Current state (2026-08-19)
Repo `/Users/kunthawat/Gitea/Chatwoot`, branch `develop`, **pushed to Gitea**.
Remote origin = `ssh://git@moreminimoreapps-gitea.ahkhwd.easypanel.host:2222/kunthawat/moreminimore-chat` (NOT github).
SSH push via port **2222** (the HTTPS path git.moreminimore.com gets nginx 413; SSH port 22 is not Gitea SSH).
HEAD = `ccff2dfca`. Worktree clean except untracked `.hermes/plans/` (never commit).
### Completed this session (all through independent five-key reviews, deployed by user)
- **Rebrand → Moreminimore Chat** complete (phases 1-3 + Dockerfile + brand DB fix): f17ce89, d925ab1, 18e6fc28a, bee28c32e, d77d60443, 4b35130ae.
- **Docker/build**: root `Dockerfile` added (4b35130ae) so EasyPanel 'Dockerfile' builder works instead of Nixpacks rbenv stack. nixpacks.toml added then removed (wrong approach).
- **M1 — per-account OpenAI custom base URL** (the /app/accounts/{id}/settings/integrations/openai page): commit f9628db0e. Adds optional custom OpenAI-compatible base_url per account (apps.yml schema+form, llm_base_service api_base priority hook.base_url→CAPTAIN_OPEN_AI_ENDPOINT→api.openai.com, key_validator base_url kwarg, hook https validation). NOTE: an earlier wrong-scope Captain-level provider switch (46fe70398) was reverted (961ecd388) — the Captain CAPTAIN_OPEN_AI_ENDPOINT already supports custom URL as original behavior, so no change needed there.
- **M2 — analytics foundation** (self-improving chatbot, admin-only):
- 9fbb0da9b `conversation_daily_metrics` (per account/day; tags, deal outcomes, peak hours, agent/team/channel/inbox breakdown)
- 0fad9ce6b `customer_daily_metrics` (per customer/day; agent_ids)
- 161e4210b `product_catalog_entries` (per-account hierarchical product reference: group>subgroup>product+aliases)
- ccff2dfca `lib/llm/resolver.rb` (LLM cascade: per-account openai hook → Captain → nil; https-only api_base)
- **DB brand fix** done by user on prod (login no longer shows Chatwoot / logo loads).
### M2 design decisions (approved by user — IMPORTANT for remaining work)
- Analyze EVERY chat of EVERY account daily (high LLM cost accepted — selling point is self-improving chatbot).
- LLM cascade: per-account openai → fallback Captain config → if neither, feature disabled (LLM integration effectively always on unless Captain unset).
- Use Chatwoot's ORIGINAL conversation tags (ActsAsTaggableOn tag_list) — do NOT add many tag fields. Product/sale tag goes into the same tag list, prefixed (e.g. lines like `group>subgroup>product` joined, or `สินค้า:...`/`กลุ่ม:...`). Daily metric `top_tags` aggregates from these.
- **No `tagged_at`** (user decided to skip time-tracking to avoid DB complexity).
- Per-day stats are IMMUTABLE snapshots; conversation live tags get re-tagged on later analysis (e.g. session now about product B → product A tag removed). The daily snapshot preserves the earlier state.
- Report page **admin-role ONLY** (agent must NOT see deep analytics). Filter by agent/team/inbox/channel/tag/sale-tag/time + cross-analysis. Break down per CUSTOMER and per AGENT.
- Weekly (Mon 10:00) LLM summarizes 7 days → persona improvement recommendation (summary only, NOT full prompt). Delivered via preferred channel: Line (has quick-replies) / Telegram (inline keyboard) / webhook (3rd party). Admin chooses approve / view-full-prompt / reject; viewing full prompt → re-approve; on approve → webhook with prompt (admin sets URL+secret).
- Product list: per-account, admin-only menu (like report), import via copy/paste or CSV/XLSX. Multi-level hierarchy (big group>subgroup>product>display) — LLM matches from lowest level first, tags ancestors; supports multiple products/groups per chat.
### NEXT TO DO (M2 remaining — biggest remaining work)
1. **part2b-ii — LLM classifier service: ✅ DONE (2026-08-19)**. New `lib/llm/analytics_classifier.rb` — reads conversation, classifies via `Llm::Resolver` cascade + `with_schema` JSON output → `Result` (topics, products via tag_hierarchy path mapping, deal won/lost/undecided). Fail-closed on no-credential (returns `disabled`, never calls LLM / sends content). Also fixed: `Account` missing `has_many :product_catalog_entries` (foundation `161e4210b` wired model+migration but not the reverse association → would NoMethodError). Verified: `ruby -c` clean (2.6/3.4), isolated smoke 8/8 under Ruby 3.4.10 (found `/opt/homebrew/opt/ruby@3.4/bin/ruby` — real 3.4.10 available, `.ruby-version`=3.4.4), static scan clean, independent reviewer `deleg_47c1a2cb` = complete 5-key PASS (non-blocking em-dash escape fix applied). Plan doc: `.hermes/plans/2026-08-19-m2-self-improving-analytics.md`.
2. **part2b-iii — daily batch job: ✅ DONE (2026-08-19)**. New `app/services/analytics/account_daily_processor.rb` (per-account/date: UTC day window from `account.reporting_timezone`, selects active conversations, classifies via `Llm::AnalyticsClassifier` fail-closed, re-tags replacing M2-managed tags + preserving manual labels, upserts ConversationDailyMetric unique account+date + CustomerDailyMetric unique account+contact+date) + `app/jobs/analytics/daily_metrics_job.rb` (per-account rescue, yesterday in account TZ) + `config/schedule.yml` entry `analytics_daily_metrics_job` `30 2 * * *` timezone `Asia/Bangkok`. Verified: `ruby -c` clean (3.4.10), smoke 15/15, static scan clean, schedule no dup keys, reviewer `deleg_fecf7c61` = complete 5-key PASS (4 non-blocking suggestions: add specs, N+1 in chat_message_count, deal_outcomes dual structure, apply_tags rescue scope).
3. **phase 2 — rollup + report: ✅ DONE (2026-08-19)**. Backend: `Analytics::ReportService` (summary/timeseries/customers/agents from immutable daily-metric tables; deal_outcomes normalization for 'totals'-nested + flat) + `Analytics::DrilldownService` (deep filterable drilldown over live conversations: since/until/agent/team/inbox/channel/tag/deal + pagination; fail-closed label-join) + `Api::V2::Accounts::AnalyticsReportsController` (admin-only via ReportPolicy#view?=administrator; summary + drilldown) + routes `analytics_reports` (get :summary/:drilldown). `AccountDailyProcessor#apply_tags` now writes `deal:<outcome>` tag for deal filtering. Frontend: `api/analyticsReports.js` + `AnalyticsReports.vue` (summary cards + filter bar + drilldown table) + route `analytics_reports` under reports section + `ANALYTICS_REPORTS` i18n keys in en/report.json. Verified: backend smoke 12/12, reviewers `deleg_6800b012` (backend) + `deleg_15f8447c` (frontend) = complete 5-key PASS (non-blocking i18n suggestions applied). NOTE: frontend full build (eslint/vite/vitest) cannot run locally (no node_modules) — run `pnpm install && pnpm test` before ship.
4. **phase 3 — import + persona eval + approval (2026-08-20):**
- **Weekly persona eval** ✅: `Analytics::WeeklyPersonaEvaluator` (7d via ReportService → LLM summary+recommendations; summary-only, no PII) + `persona_evaluation` endpoint + route. Reviewer `deleg_b7d63fd7` PASS.
- **Frontend product-catalog import UI** ✅: `api/productCatalog.js` + `productCatalog/Index.vue` (paste + xlsx/csv upload + result + table) + admin-only route. Reviewer `deleg_0e9f9291` PASS (i18n suggestions applied).
- **Approval flow** (user chose **LINE primary + webhook fallback**, then added **Telegram**; precedence LINE → Telegram → webhook): `Analytics::PersonaApprovalService` (`deliver` LINE quick-reply / Telegram inline keyboard / webhook fallback; `notify_approval` webhook-only w/ decision) + `Analytics::WeeklyPersonaEvaluationJob` + controller (`persona_evaluation_deliver`, `persona_approval_settings` [line_user_id + telegram_chat_id + webhook_url, conditional], `persona_decision`) + routes + schedule.yml `analytics_weekly_persona_evaluation_job` (Mon 10:00 Asia/Bangkok). First reviewer `deleg_1fe19871` FAIL-CLOSED (1 medium logic error: `:approve` path LINE-first + decision not passed) → **FIXED** (webhook-only `notify_approval(decision)`) → re-review `deleg_3052f7a4` PASS; Telegram `deleg_34116d1a` PASS. Config: `persona_line_user_id` / `persona_telegram_chat_id` / `persona_webhook_url` / `persona_webhook_secret` in Account#custom_attributes.
- **Docs** — via HANDOFF.md/plan doc; no separate docs/ convention.
- **POST-MVP design note (prompt webhook handshake) — user clarified, NOT built, awaiting LLM-server details**: a SEPARATE LLM server holds/applies the actual prompt; this app connects via webhook — (1) connect-first: pull current prompt (GET) + store snapshot; (2) approve weekly recommendation → POST new prompt to LLM server to apply. Analytics here ONLY surfaces "what customers asked" + recommendations; knowledge-prep/prompt-authoring is admin/LLM-server side. Delivery-webhook design (REST 2-endpoint vs generic action-body) DEFERRED — user wants document/design only for now.
Verified: smoke tests (eval 8/8, import 10/10 + xlsx 5/5, approval 10/10 incl. Telegram), `ruby -c` clean, node --check on .js, schedule.yml valid + unique keys, static scan clean across all.
### Verification notes / blockers
- Ruby env active is 2.6.10; target 3.4.4. Rails/RSpec/RuboCop cannot be reliably claimed passed. Ruby `ruby -c` syntax + YAML validity + isolated smoke tests used instead.
- Full Vitest baseline is clean under TZ=UTC: 414 files / 4176 tests, 0 failed.
- Enterprise (EE) files still read CAPTAIN_OPEN_AI_ENDPOINT directly and won't honor per-account base_url gating — intentional (out of OSS scope), note as follow-up.
- No secrets committed. Temporary test helpers under /tmp only.
## ───────── historical (2026-08-16, before this session) ─────────
### Latest handoff — 2026-08-16 (updated after rebrand + push)
- Rebrand to **Moreminimore Chat** complete across all phases (committed): `f17ce89` (phase 1 configurable brand core: logo/config/mailer defaults/views), `d925ab1` (phase 2a cleanup + 512px thumbnail), `18e6fc28a` (phase 2b color theme: black #333333 primary + Moreminimore yellow #FED400 accent), `bee28c32e` (phase 3 visible surfaces: app.json + mailers + 56 locales + prompts). All phases passed independent five-key reviews (deleg_a733cdf4, deleg_37455fbd, deleg_9b054593, deleg_892d115f).
- **PUSHED to Gitea** via SSH port 2222: branch `develop` (HEAD `bee28c32e`) now on `kunthawat/moreminimore-chat`. Remote `origin` = `ssh://git@moreminimoreapps-gitea.ahkhwd.easypanel.host:2222/kunthawat/moreminimore-chat`. HTTPS push to `git.moreminimore.com` is blocked by nginx 413 (client_max_body_size < ~237MB pack); SSH port 22 is NOT Gitea SSH — use 2222. `upstream` (github.com/chatwoot) removed; Gitea rejects shallow push, so unshallow first. SSH key `moreminimore-local-ed25519` added via Gitea API.
- Earlier checkpoints: `19dc449` SM-07, `2ef6fa5` SM-07 remediation (approved deleg_087e4a3f), `364e72f` SM-08, `8101395` residual Hub removal (approved deleg_49d6ee2e + deleg_58a576e1).
### Verified checkpoints
- `8ebb320` — privacy/branding audit harness.
- `832a7fd` — Hub sync/registration/telemetry removal.
- `1a3697f` — restored only the compatibility APIs required by direct push/billing callers.
- `8b1a033` — removed the Chatwoot Hub push relay; exact post-commit independent review passed.
- `3458272` — removed cwctl telemetry from the installer; exact post-commit independent review passed.
- `d9bf4c4` — removed community signup/onboarding website-branding enrichment; split exact post-commit reviews `deleg_5e2578c5` (backend) and `deleg_ee3e217f` (frontend) passed with empty blocking arrays.
- `ced77af` — SM-06 Product Analytics removal and dashboard config allowlist remediation; pre-commit review `deleg_f9644984` and exact post-commit review `deleg_9b0a2f63` passed with empty security/logic arrays.
### Current work / uncheckpointed work
- SM-05 community signup enrichment removal is checkpointed at `d9bf4c4`; pre-commit review, commit hooks, and split exact post-commit reviews passed. The enterprise service/spec remain unchanged and fail with `NameError: uninitialized constant WebsiteBrandingService`. This remains an explicit architecture/legal blocker; do not add a dummy service or restore remote enrichment.
- SM-06 is verified at `ced77af`. Its follow-up fix applies `.slice(*GLOBAL_CONFIG_KEYS)` before merging `app_config`, preventing stale/unrequested values such as `CLOUD_ANALYTICS_TOKEN` from reaching serialized `window.globalConfig`. SM-07 local-only observability and the remaining SM-08 dashboard/config script-injection work are still uncheckpointed. The community Help Center SM-08 correction is committed as `5619cc3`: remove GTM remote-container execution, reject/remove legacy GTM config, and retain only fixed allow-listed provider snippets for explicit admin configuration. Exact post-commit review `deleg_dd3db5b7` passed with empty blocking arrays. Enterprise marketing conversion tracking remains outside this community scope pending legal/compatibility review.
### Evidence and blockers
- SM-03 exact post-commit review passed for `1a3697f..8b1a033`; reviewer suggestions are to add explicit missing/blank/partial Firebase negative coverage and clarify legacy FCM configuration names in `.env.example`.
- SM-04 exact post-commit review passed for `1a3697f..3458272`; Bash 3.2 compatibility and fail-closed privacy-test behavior were verified.
- SM-05 exact committed range `3458272..d9bf4c4` passed split independent reviews: backend `deleg_5e2578c5` and frontend `deleg_ee3e217f`; both returned complete five-key verdicts with empty security and logic arrays. Frontend targeted run passed 9 suites/8 tests. Backend RSpec rerun is environment-blocked because Ruby 3.4.4 is required but only 3.4.10 is installed and the bundle has no usable `rspec` executable.
- SM-06 exact committed range `a96b977..ced77af` changed one controller line and passed fresh pre-commit/post-commit independent review. Raw merge leaked a stubbed analytics token; the allowlist slice now removes unrequested keys before layout serialization. Committed Ruby syntax, diff check, and security scan passed. Rails RSpec/RuboCop remain blocked because the active Ruby is 2.6.10 and Bundler 2.5.16 is unavailable.
- SM-08 exact post-commit review `deleg_dd3db5b7` passed for `d9bf4c4..5619cc3`; 57 portal model/controller examples, GTM regression 3/3, Ruby/ERB syntax, RuboCop, ESLint/Prettier, Vite build and privacy-audit harness passed. Full Vitest with `TZ=UTC` passes 414 files / 4176 tests, 0 failed.
- No push or deploy has occurred.
- Approved product name, domains, logos/icons and sender/legal values are still missing; stop before SM-09/10.
- Full Rails/frontend suites, production-like DB checks, HTTP/DNS egress capture, `git fetch --unshallow`, and upstream merge rehearsal remain outstanding. The full Vitest frontend baseline is clean under `TZ=UTC`: 414 files / 4176 tests passed, 0 failed (the earlier "407/15 failed" record was a non-UTC run artifact; timezone-sensitive tests all pass under UTC).
- Temporary test services/configuration stay under `/tmp`; never commit them or secrets.
### Repository baseline
- Path: `/Users/kunthawat/Gitea/Chatwoot`
- Origin: `https://github.com/chatwoot/chatwoot.git`
- Branch: `develop`
- Initial baseline commit: `9a73c1473ffa0ae6a9c7725046b8ca17922dcc83`
- Latest checkpoint: `ced77af` (SM-06 Product Analytics plus dashboard config allowlist remediation; exact post-commit review `deleg_9b0a2f63` passed). SM-08 Help Center remains separately verified at `5619cc3`.
- Clone is shallow; fetch full upstream history before merge/rebase rehearsal.
### Primary deliverable
- `.hermes/plans/2026-08-15_092534-chatwoot-private-rebrand.md`
- Small-model execution index: `.hermes/plans/chatwoot-private/README.md`
- The execution package contains `00-execution-contract.md` plus runbooks `01``12` with 58 unique `SM-*` tasks. Send only one task at a time and review its exact diff/test evidence before advancing.
### Most important finding
`DISABLE_TELEMETRY=true` is not a complete opt-out. `lib/chatwoot_hub.rb` still posts `instance_config` to the Hub; it only omits `instance_metrics`. The corresponding behavior is asserted in `spec/lib/chatwoot_hub_spec.rb`.
### Other unsolicited/vendor-controlled egress to remove
- Chatwoot Hub daily sync and onboarding registration
- Chatwoot Hub `/send_push` relay fallback
- Hub-hosted changelog fetch
- silent cwctl command reporting to the Hub
- automatic signup email-domain website/DNS branding enrichment
- Amplitude browser analytics
- Sentry/frontend/backend remote error reporting
- optional remote APM agents
- arbitrary dashboard scripts and remote GTM container execution
- enterprise marketing conversion tracking (proprietary/cloud-only; pending separate legal/compatibility decision)
### Implementation order
1. Follow dependency order in `.hermes/plans/chatwoot-private/README.md`.
2. Establish the baseline and privacy audit harness before behavior changes.
3. Remove Hub sync/registration/manual refresh, push relay, cwctl telemetry, signup-domain enrichment and remote changelog.
4. Neutralize/remove analytics, remote error reporting/APM and script injection.
5. Stop at the Runbook 09 input gate until approved brand values/assets are supplied.
6. Add central brand defaults and replace visible assets/copy without renaming compatibility APIs.
7. Run static and isolated dynamic egress gates, positive feature-traffic tests, full review and visual QA.
8. Complete upstream-history and merge-rehearsal workflow; never mix upstream merge with customization commits.
### User decisions required before implementation
- Product name, canonical/support/docs domains, logos/icons and email sender name
- Private Gitea repository name
- Whether to keep deprecated widget API aliases (recommended: yes)
- Whether local-only logs are sufficient or a self-hosted observability endpoint is required
- Push notification plan and own FCM/VAPID credentials
### Safety/legal notes
- Root is MIT, but `enterprise/` is proprietary; do not ship enterprise features without a separate license.
- No remote repository was created, no files were pushed and no deployment was performed.