By Sagar Shankaran, Founder of CallSphere
Design and implement an AI copilot that understands your SaaS product context, proactively offers suggestions, and lets users maintain full control over all actions.
Key takeaways
A chatbot waits for questions. A copilot watches what you are doing and offers help before you ask. When you are writing an email in your CRM, the copilot suggests a follow-up template based on the deal stage. When you are building a report, it recommends which metrics to include based on your audience.
The key architectural difference is context capture. A copilot needs a continuous stream of user activity to generate relevant suggestions.
The copilot system has three components: a context collector on the frontend, a suggestion engine on the backend, and a presentation layer that shows suggestions without disrupting the user's workflow.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent for IT support in your browser — 60 seconds, no signup.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
// Frontend context collector
interface CopilotContext {
page: string;
action: string;
entityType?: string;
entityId?: string;
formData?: Record<string, unknown>;
selectionText?: string;
timestamp: number;
}
class CopilotContextCollector {
private buffer: CopilotContext[] = [];
private ws: WebSocket;
private flushInterval: ReturnType<typeof setInterval>;
constructor(wsUrl: string, authToken: string) {
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.ws.send(JSON.stringify({ type: "auth", token: authToken }));
};
// Flush context every 2 seconds to avoid spamming
this.flushInterval = setInterval(() => this.flush(), 2000);
}
track(ctx: Omit<CopilotContext, "timestamp">) {
this.buffer.push({ ...ctx, timestamp: Date.now() });
}
private flush() {
if (this.buffer.length === 0) return;
this.ws.send(JSON.stringify({ type: "context", events: this.buffer }));
this.buffer = [];
}
destroy() {
clearInterval(this.flushInterval);
this.ws.close();
}
}
The suggestion engine receives context events, maintains a rolling window of user activity, and generates suggestions when activity patterns match known triggers.
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from collections import deque
import asyncio
@dataclass
class UserSession:
user_id: str
tenant_id: str
context_window: deque = field(default_factory=lambda: deque(maxlen=50))
last_suggestion_time: datetime = field(default_factory=datetime.utcnow)
class SuggestionEngine:
def __init__(self, llm_client, min_suggestion_interval: int = 30):
self.sessions: dict[str, UserSession] = {}
self.llm_client = llm_client
self.min_interval = timedelta(seconds=min_suggestion_interval)
def get_session(self, user_id: str, tenant_id: str) -> UserSession:
if user_id not in self.sessions:
self.sessions[user_id] = UserSession(
user_id=user_id, tenant_id=tenant_id
)
return self.sessions[user_id]
async def process_context(self, user_id: str, tenant_id: str,
events: list[dict]) -> dict | None:
session = self.get_session(user_id, tenant_id)
for event in events:
session.context_window.append(event)
# Rate limit suggestions
now = datetime.utcnow()
if now - session.last_suggestion_time < self.min_interval:
return None
trigger = self.detect_trigger(session)
if not trigger:
return None
suggestion = await self.generate_suggestion(session, trigger)
session.last_suggestion_time = now
return suggestion
def detect_trigger(self, session: UserSession) -> str | None:
recent = list(session.context_window)[-5:]
if not recent:
return None
latest = recent[-1]
# Trigger: user is editing a form for more than 30 seconds
if latest.get("action") == "form_edit":
edit_events = [e for e in recent if e.get("action") == "form_edit"]
if len(edit_events) >= 3:
return "form_assistance"
# Trigger: user is viewing a record with incomplete data
if latest.get("action") == "view" and latest.get("entityType"):
return "record_insight"
return None
async def generate_suggestion(self, session: UserSession,
trigger: str) -> dict:
context_summary = self.summarize_context(session)
prompt = f"""Based on the user's activity, generate a helpful suggestion.
Trigger: {trigger}
Context: {context_summary}
Respond with JSON: {{"title": "...", "body": "...", "actions": [...]}}"""
response = await self.llm_client.chat(
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
return response
def summarize_context(self, session: UserSession) -> str:
recent = list(session.context_window)[-10:]
lines = []
for event in recent:
lines.append(
f"[{event.get('action')}] on {event.get('entityType', 'page')}"
f" ({event.get('page', '/')})"
)
return "\n".join(lines)
Suggestions should appear in a non-modal side panel. Users must always be able to dismiss, accept, or modify them.
// React copilot suggestion component
import { useState, useEffect } from "react";
interface Suggestion {
id: string;
title: string;
body: string;
actions: { label: string; action: string; payload?: Record<string, unknown> }[];
}
export function CopilotPanel({ ws }: { ws: WebSocket }) {
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
useEffect(() => {
const handler = (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data.type === "suggestion") {
setSuggestions((prev) => [data.suggestion, ...prev].slice(0, 5));
}
};
ws.addEventListener("message", handler);
return () => ws.removeEventListener("message", handler);
}, [ws]);
const dismiss = (id: string) => {
setSuggestions((prev) => prev.filter((s) => s.id !== id));
ws.send(JSON.stringify({ type: "feedback", suggestion_id: id, action: "dismiss" }));
};
const accept = (id: string, action: string) => {
ws.send(JSON.stringify({ type: "feedback", suggestion_id: id, action: "accept" }));
// Execute the action through your app's action system
executeAction(action);
dismiss(id);
};
return (
<div className="w-80 border-l bg-gray-50 p-4 overflow-y-auto">
<h3 className="font-semibold text-sm text-gray-600 mb-3">Copilot Suggestions</h3>
{suggestions.map((s) => (
<div key={s.id} className="bg-white rounded-lg shadow-sm p-3 mb-2">
<h4 className="font-medium text-sm">{s.title}</h4>
<p className="text-xs text-gray-600 mt-1">{s.body}</p>
<div className="flex gap-2 mt-2">
{s.actions.map((a) => (
<button key={a.label} onClick={() => accept(s.id, a.action)}
className="text-xs bg-blue-600 text-white px-2 py-1 rounded">
{a.label}
</button>
))}
<button onClick={() => dismiss(s.id)}
className="text-xs text-gray-400 ml-auto">Dismiss</button>
</div>
</div>
))}
</div>
);
}
Every copilot suggestion must be an offer, never an automatic action. Users must be able to dismiss any suggestion, disable the copilot entirely, and configure what triggers suggestions. Store preferences per user and respect them on every request.
# User preference storage for copilot behavior
async def get_copilot_preferences(db, user_id: str) -> dict:
row = await db.fetchrow(
"SELECT preferences FROM copilot_settings WHERE user_id = $1",
user_id
)
defaults = {
"enabled": True,
"triggers": ["form_assistance", "record_insight", "workflow_tip"],
"frequency": "normal", # low, normal, high
"dismissed_categories": [],
}
if not row:
return defaults
stored = row["preferences"]
return {**defaults, **stored}
Implement three controls: a minimum interval between suggestions (30-60 seconds), a daily suggestion cap per user, and a feedback loop that tracks dismissal rates. If a user dismisses more than 70% of a specific suggestion type, stop showing that type automatically.
Still reading? Stop comparing — try CallSphere live.
See the IT support AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
The copilot should only access data the user can already see. Use the same permission system as your main application. Additionally, avoid sending sensitive fields (SSNs, passwords, API keys) to the LLM even if the user has access — redact them before context injection.
Track three metrics: suggestion acceptance rate (target above 30%), time saved per accepted suggestion (measure task completion time with and without the copilot), and user satisfaction via periodic micro-surveys embedded in the copilot panel.
#AICopilot #SaaS #ContextAwareAI #SuggestionEngine #Python #TypeScript #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Chengdu, Beijing, and Shenzhen SaaS teams lose trials to slow follow-up. CallSphere qualifies inbound leads 24/7 in Mandarin and English and books demos straight into the calendar.
Your Estonian SaaS scales globally, but your support line runs on one founder and a shared inbox. Here is how CallSphere AI voice and chat agents cover inbound demos and support 24/7 for Tallinn and Tartu tech teams.
The public MCP registry crossed 9,400 servers in April 2026. Here is a curated walkthrough of the SaaS MCP servers CallSphere mounts in production, with OAuth 2.1 PKCE patterns.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.
Mastra.ai is becoming the go-to TypeScript agent framework in 2026. Workflows, RAG, evals, and an honest comparison with Vercel AI SDK 5 for serious teams.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco