From 8325d2e028c59bd835a59cf8e2101784279cfed7 Mon Sep 17 00:00:00 2001 From: Shakes <7453460+dshakes@users.noreply.github.com> Date: Tue, 19 May 2026 23:08:29 -0400 Subject: [PATCH 001/742] WhatsApp: agent avatar + "my voice" persona + handoff disclosure (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agents: avatar_url + style_prompt columns; threaded through REST CreateAgent / GetAgent / UpdateAgent. - Dashboard: new Identity card on the agent detail page (avatar URL with live preview + style textarea). Agent activity timeline renders the avatar next to agent_reply rows. - Bridge: one-shot handoff disclosure per contact ("Hey, X's assistant here…") with persisted disclosed-jids set; configurable via LANTERN_AGENT_HANDOFF_MESSAGE. - Persona: tightened baseline (<=15 words, plain text, no markdown, no em-dashes, contractions, no greetings/signoffs). Accepts ownerSamples + stylePrompt + disclosed; relaxes the strict impersonation rule post-disclosure. - Few-shot voice: bridge captures last 10 owner-sent messages per contact (skips commands/self-chat/groups), persists across restarts, and injects them as exemplars. Bridge also fetches style_prompt from control-plane with a 30s TTL cache. --- .../agents/[name]/channels/whatsapp/page.tsx | 4 +- .../app/(dashboard)/agents/[name]/page.tsx | 97 +++++++++++++++++++ apps/web/components/whatsapp-pairing.tsx | 43 +++++++- apps/web/lib/api.ts | 2 +- apps/web/lib/mock-data.ts | 3 + services/control-plane/internal/db/migrate.go | 21 ++++ .../control-plane/internal/handlers/rest.go | 96 ++---------------- services/whatsapp-bridge/src/agent.ts | 33 +++++++ services/whatsapp-bridge/src/natural.ts | 82 ++++++++++++---- services/whatsapp-bridge/src/session.ts | 26 ++++- 10 files changed, 296 insertions(+), 111 deletions(-) diff --git a/apps/web/app/(dashboard)/agents/[name]/channels/whatsapp/page.tsx b/apps/web/app/(dashboard)/agents/[name]/channels/whatsapp/page.tsx index 4f689c5c..78089722 100644 --- a/apps/web/app/(dashboard)/agents/[name]/channels/whatsapp/page.tsx +++ b/apps/web/app/(dashboard)/agents/[name]/channels/whatsapp/page.tsx @@ -26,6 +26,7 @@ import { useParams } from "next/navigation"; import Link from "next/link"; import { ArrowLeft } from "lucide-react"; import { useAuth } from "@/lib/auth"; +import { useAgent } from "@/lib/hooks"; import { WhatsAppPairing } from "@/components/whatsapp-pairing"; export default function AgentWhatsAppPage() { @@ -33,6 +34,7 @@ export default function AgentWhatsAppPage() { const name = params?.name ?? ""; const { user } = useAuth(); const tenantId = user?.tenantId ?? "default"; + const { agent } = useAgent(name); return (
@@ -54,7 +56,7 @@ export default function AgentWhatsAppPage() {
- + ); diff --git a/apps/web/app/(dashboard)/agents/[name]/page.tsx b/apps/web/app/(dashboard)/agents/[name]/page.tsx index 49ef46c0..a9c17c4e 100644 --- a/apps/web/app/(dashboard)/agents/[name]/page.tsx +++ b/apps/web/app/(dashboard)/agents/[name]/page.tsx @@ -53,6 +53,7 @@ import { Cloud, Download, GitBranch, + Bot, } from "lucide-react"; import clsx from "clsx"; import { api } from "@/lib/api"; @@ -198,6 +199,14 @@ export default function AgentDetailPage() { const { agent, loading: agentLoading, error: agentError } = useAgent(name); const { runs: agentRuns, loading: runsLoading, refresh: refreshRuns } = useAgentRuns(name); + // Hydrate identity fields from the API once the agent payload lands. + useEffect(() => { + if (!agent) return; + setAvatarUrl(agent.avatarUrl ?? ""); + setStylePrompt(agent.stylePrompt ?? ""); + setIdentityDirty(false); + }, [agent]); + // Setup-gate status. ready=false means the agent has required connectors // or surfaces that aren't connected — we banner the page and disable Run. // Templated agents store their requirements in labels JSONB; non-template @@ -228,6 +237,13 @@ export default function AgentDetailPage() { const [promptDirty, setPromptDirty] = useState(false); const [savingPrompt, setSavingPrompt] = useState(false); + // Identity — avatar URL shown in the WhatsApp dashboard timeline; style + // prompt is the per-agent "my voice" override fed into the bridge persona. + const [avatarUrl, setAvatarUrl] = useState(""); + const [stylePrompt, setStylePrompt] = useState(""); + const [identityDirty, setIdentityDirty] = useState(false); + const [savingIdentity, setSavingIdentity] = useState(false); + // Test state const [testInput, setTestInput] = useState(""); const [testModel, setTestModel] = useState("auto"); @@ -587,6 +603,19 @@ export default function AgentDetailPage() { try { await api.deleteAgent(name); toast.success(`Agent "${name}" deleted`); router.push("/agents"); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete"); } finally { setDeleting(false); setShowDeleteConfirm(false); } }, [name, toast, router]); + const handleSaveIdentity = useCallback(async () => { + setSavingIdentity(true); + try { + await api.updateAgent(name, { avatarUrl, stylePrompt }); + setIdentityDirty(false); + toast.success("Identity saved"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Save failed"); + } finally { + setSavingIdentity(false); + } + }, [name, avatarUrl, stylePrompt, toast]); + const handleSavePrompt = useCallback(async () => { setSavingPrompt(true); try { @@ -1118,6 +1147,74 @@ export default function AgentDetailPage() { /> + {/* Identity — avatar + per-agent style ("my voice"). Used by the + WhatsApp dashboard timeline and the bridge persona. */} +
+
+

+ Identity + -- avatar + voice shown in WhatsApp + chat +

+ +
+
+
+ {avatarUrl ? ( + Agent avatar { (e.currentTarget as HTMLImageElement).style.display = "none"; }} + /> + ) : ( +
+ +
+ )} +

Preview

+
+
+
+ + { setAvatarUrl(e.target.value); setIdentityDirty(true); }} + placeholder="https://example.com/avatar.png" + className="w-full rounded-lg border border-zinc-800 bg-surface-0 px-3 py-2 text-sm text-zinc-300 placeholder:text-zinc-600 outline-none focus:border-lantern-500/50 focus:ring-1 focus:ring-lantern-500/20" + /> +
+
+ +