Skip to Content
InsightsInsights — Accepted Learnings

Insights — Accepted Learnings

Status: [To Build]

When a human reviews a generated insight, they can accept individual items (strengths, weaknesses, opportunities, recommendations) and optionally add a comment. Accepted items are stored in the tenant’s channel_insights RAG dataset and become available to all other agents as verified institutional knowledge about the client’s marketing performance.


Why This Matters

Without this loop, every insight run starts from scratch. The same observations get regenerated week after week with no cumulative memory. With accepted learnings:

  • The strategy writer knows “the client’s GSC branded CTR has historically been strong — focus non-branded terms” because a human verified it last quarter.
  • The report writer can cite prior accepted observations and track whether they’ve improved.
  • Future insight agents pre-load past accepted insights and build on them rather than repeating them — the analysis deepens each cycle rather than resetting.

The Acceptance Flow

Insight generated → status: "done" Human reviews insight items in the dashboard (Screen I2 or I3) ├─ Accepts an item (optionally adds a comment) │ │ │ ▼ │ POST /tenant/v1/insights/:insightId/accept │ { sectionKey, itemIndex, comment? } │ │ │ ▼ │ ChannelInsightAcceptance record created │ │ │ ▼ │ RAG ingestion job enqueued → queue: 'rag:ingestion' │ { type: 'content', content: <formatted text>, tenantId, datasetId: channel_insights } │ │ │ ▼ │ Item indexed in Qdrant → available to all agents └─ Dismisses (or ignores) an item → no action taken

Acceptance is per item, not per insight report. A report with 12 items might have 4 accepted and 8 dismissed. Each accepted item becomes a separate RAG chunk.


Database Schema

New model: ChannelInsightAcceptance

Add to packages/db/prisma/schema.prisma:

model ChannelInsightAcceptance { id String @id @default(cuid()) tenantId String channelInsightId String connectedChannelId String channelType String sectionKey String // "strengths" | "weaknesses" | "opportunities" | "recommendations" itemIndex Int // Index of the item in the section array title String // Copied from the insight item at acceptance time detail String @db.Text // Copied from the insight item at acceptance time priority String? // Only set for "recommendations": "high" | "medium" | "low" period String // Period of the parent ChannelInsight, e.g. "last_6_months" reviewerComment String? @db.Text // Optional note from the human reviewer acceptedByUserId String // The user who accepted the item ragFileId String? // Set after RAG ingestion completes — links to rag_files.id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) channelInsight ChannelInsight @relation(fields: [channelInsightId], references: [id], onDelete: Cascade) @@index([tenantId]) @@index([channelInsightId]) @@index([tenantId, channelType]) @@map("channel_insight_acceptance") }

Also add the reverse relation to ChannelInsight:

// Inside ChannelInsight model: acceptances ChannelInsightAcceptance[]

RAG Dataset: channel_insights

A fifth standard dataset added to every tenant’s RAG setup alongside the existing four. Created automatically when the tenant is provisioned (same as client_docs, etc.).

// Added to STANDARD_DATASETS in packages/feature-knowledge/src/knowledge.types.ts { name: 'channel_insights', description: 'Accepted insight observations from channel analysis — human-verified performance learnings', allowedAgentRoles: [ // All insight agents — so they build on prior accepted observations 'gsc-insights', 'ga-insights', 'google-ads-insights', 'meta-ads-insights', 'facebook-insights', 'instagram-insights', 'linkedin-insights', 'gbp-insights', // Content and strategy agents that benefit from knowing what's working 'strategy-writer', 'report-writer', 'ads-analyst', 'activity-planner', 'blog-writer', // Legacy role names 'strategy', 'data_analyst', ], useLocalEmbedding: false, }

Ingested Text Format

Each accepted item is ingested as a single type: 'content' RAG job. The text passed to the ingestion pipeline is formatted as:

[CHANNEL INSIGHT — {channelType} — {month} {year}] Section: {sectionKey} | Period: {period} {priority badge if recommendations: "Priority: HIGH | "}Accepted by: {userName} {title} {detail} {if reviewerComment: "Reviewer note: {reviewerComment}"}

Example:

[CHANNEL INSIGHT — Google Search Console — Apr 2026] Section: opportunities | Period: last_6_months Accepted by: Sarah K. 12 keywords sitting in positions 4–10 These are within 1–2 optimisation cycles of page 1. Combined they represent ~800 additional clicks/month if moved up. Top targets: "emergency plumber sydney" (pos 7, 4,200 impressions), "plumber north sydney" (pos 8, 1,100 impressions), "blocked drain sydney" (pos 6, 890 impressions). Reviewer note: Focus on the 3 plumbing service terms first — highest commercial intent.

This format is designed to be retrieved meaningfully. An agent querying "what GSC opportunities have been identified for this client?" will surface this chunk with high recall because the channel type, section category, and key metric values are embedded in the text.


API Endpoint

Accept an insight item

POST /tenant/v1/insights/:insightId/accept

Request body:

{ sectionKey: "strengths" | "weaknesses" | "opportunities" | "recommendations"; itemIndex: number; // Index in the section array (0-based) comment?: string; // Optional reviewer note }

Behaviour:

  1. Load ChannelInsight — validate tenant ownership and status === "done" (can’t accept items from a pending/failed insight)
  2. Extract the item at insights.sections[sectionKey][itemIndex] — return 404 if out of bounds
  3. Check for duplicate: if a ChannelInsightAcceptance already exists for this channelInsightId + sectionKey + itemIndex, return the existing record (idempotent)
  4. Create ChannelInsightAcceptance record
  5. Enqueue rag:ingestion job:
    { type: 'content', content: <formatted text block>, fileId: acceptanceRecord.id, // used as the logical file ID in rag_files tenantId, datasetId: <channel_insights dataset id>, metadata: { source: 'channel_insight_acceptance', channelType: insight.channelType, sectionKey, acceptedAt: new Date().toISOString(), } }
  6. After ingestion completes, the worker updates ChannelInsightAcceptance.ragFileId
  7. Return the created ChannelInsightAcceptance record

Response:

{ id: string; sectionKey: string; itemIndex: number; title: string; ragQueued: true; }

Dismiss (undo accept)

DELETE /tenant/v1/insights/:insightId/accept/:acceptanceId

Deletes the ChannelInsightAcceptance record and removes the associated vectors from Qdrant (via the existing DELETE /api/knowledge/datasets/:id/files/:fileId flow). This allows a human to correct a mistaken acceptance.

List accepted items for an insight

GET /tenant/v1/insights/:insightId/acceptances

Returns all ChannelInsightAcceptance records for the insight, useful for rendering the accepted/not-accepted state in the UI.


How Agents Use Accepted Learnings

Insight agents (pre-load pattern)

Each insight worker pre-loads prior accepted items from the channel_insights dataset for the same channelType before building the Claude prompt:

// Inside e.g. gsc-insights.worker.ts, after fetching fresh metrics: const priorLearnings = await searchService.search({ tenantId, datasetId: channelInsightsDataset.id, query: `Google Search Console observations ${tenantName}`, topK: 10, agentRole: 'gsc-insights', });

Injected in the prompt as a PRIOR ACCEPTED INSIGHTS block:

PRIOR ACCEPTED INSIGHTS (human-verified observations from previous analyses): ───────────────────────────────────────────────────────────────────────────── [CHANNEL INSIGHT — Google Search Console — Jan 2026] Section: opportunities | Period: last_6_months ... [CHANNEL INSIGHT — Google Search Console — Oct 2025] Section: weaknesses | Period: last_6_months ... ───────────────────────────────────────────────────────────────────────────── Use these as context. Reference them if they are still relevant (e.g. "This opportunity was flagged in January — position has now improved from 7 to 5"). Do not repeat them verbatim as new findings unless supported by the current data.

This prevents the insight agent from recycling the same observations every week and lets it track trend progress over time.

These agents search the channel_insights dataset via the standard rag_search pre-load pattern, alongside client_docs and published_content. No special handling needed — the accepted learnings appear as ranked search results when the agent queries for performance context.


UI Changes

Insight item cards (Screens I2 and I3)

Each item in the four section groups gains an acceptance control:

┌── ✓ Strong branded keyword performance ─────────────────────────────────┐ │ "acme plumbing" and "acme services" hold positions 1.2 and 1.4 with │ │ CTRs of 18% and 15% — well above the 3% average for branded terms. │ │ │ │ [Add note... ] [Accept Learning ✓] │ └──────────────────────────────────────────────────────────────────────────┘

Accepted state:

┌── ✓ Strong branded keyword performance ─────────────────────── [Saved ✓] ┐ │ "acme plumbing" and "acme services" hold positions 1.2 and 1.4 with │ │ CTRs of 18% and 15% — well above the 3% average for branded terms. │ │ │ │ Note: "Continue monitoring — watch for competitor activity on brand" │ │ [Undo] │ └───────────────────────────────────────────────────────────────────────────┘

The “Accept Learning” button calls POST /tenant/v1/insights/:insightId/accept. The “Undo” button calls DELETE /tenant/v1/insights/:insightId/accept/:acceptanceId.

The note field is optional and submitted together with the acceptance (not separately saved).

Insights list page (Screen I1) — card addition

Show a count of accepted learnings on each insight card:

│ ✓ 3 Strengths ✗ 2 Weaknesses │ │ ◆ 4 Opportunities → 5 Recommendations │ │ │ │ 💾 4 learnings saved to knowledge base │

Implementation Phase

This feature is Phase 7 in the implementation plan (after Phase 6 prompt tuning).

Tasks

  1. Add ChannelInsightAcceptance model to packages/db/prisma/schema.prisma
  2. Add channel_insights to STANDARD_DATASETS in packages/feature-knowledge
  3. Add POST /accept and DELETE /accept/:id endpoints to insights.routes.ts
  4. Add GET /:insightId/acceptances endpoint
  5. Update insight workers to pre-load prior accepted items from channel_insights and inject them as a PRIOR ACCEPTED INSIGHTS block in the prompt
  6. Update strategy-writer and report-writer workers to include channel_insights in their pre-load dataset list
  7. Add accept/undo UI controls to insight item cards (Screens I2 and I3)
  8. Add “N learnings saved” count to Screen I1 insight cards

Verify

  • Accepting an item creates a ChannelInsightAcceptance record and a rag_files entry with source: 'channel_insight_acceptance'
  • The ingested chunk appears in Qdrant with correct tenantId filter
  • Re-running an insight agent pre-loads and injects prior accepted items in the prompt
  • Undoing an acceptance removes the Qdrant vectors
  • Accepting the same item twice is idempotent (returns existing record)

© 2026 Leadmetrics — Internal use only