Skip to content

Commit 03d9625

Browse files
committed
Implement agent profile generation and API integration
- Added a new SQL migration for the agent profile table to store display name, status, bio, and avatar seed. - Updated the API to include a new endpoint for retrieving agent profiles. - Enhanced the agent summary structure to include the generated profile data. - Implemented profile generation logic in the cortex, utilizing identity and memory context to create a personalized agent profile. - Introduced new prompts for generating agent profiles based on context, improving the overall user experience.
1 parent c68fa96 commit 03d9625

10 files changed

Lines changed: 445 additions & 47 deletions

File tree

interface/src/api/client.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const API_BASE = "/api";
1+
const API_BASE = ((window as any).__SPACEBOT_BASE_PATH || "") + "/api";
22

33
export interface StatusResponse {
44
status: string;
@@ -226,6 +226,20 @@ export interface AgentOverviewResponse {
226226
latest_bulletin: string | null;
227227
}
228228

229+
export interface AgentProfile {
230+
agent_id: string;
231+
display_name: string | null;
232+
status: string | null;
233+
bio: string | null;
234+
avatar_seed: string | null;
235+
generated_at: string;
236+
updated_at: string;
237+
}
238+
239+
export interface AgentProfileResponse {
240+
profile: AgentProfile | null;
241+
}
242+
229243
export interface AgentSummary {
230244
id: string;
231245
channel_count: number;
@@ -234,6 +248,7 @@ export interface AgentSummary {
234248
activity_sparkline: number[];
235249
last_activity_at: string | null;
236250
last_bulletin_at: string | null;
251+
profile: AgentProfile | null;
237252
}
238253

239254
export interface InstanceOverviewResponse {
@@ -894,6 +909,8 @@ export const api = {
894909
channel_id: channelId ?? null,
895910
}),
896911
}),
912+
agentProfile: (agentId: string) =>
913+
fetchJson<AgentProfileResponse>(`/agents/profile?agent_id=${encodeURIComponent(agentId)}`),
897914
agentIdentity: (agentId: string) =>
898915
fetchJson<IdentityFiles>(`/agents/identity?agent_id=${encodeURIComponent(agentId)}`),
899916
updateIdentity: async (request: IdentityUpdateRequest) => {

interface/src/router.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,10 @@ const routeTree = rootRoute.addChildren([
275275
channelRoute,
276276
]);
277277

278-
export const router = createRouter({routeTree});
278+
export const router = createRouter({
279+
routeTree,
280+
basepath: (window as any).__SPACEBOT_BASE_PATH || "/",
281+
});
279282

280283
declare module "@tanstack/react-router" {
281284
interface Register {

interface/src/routes/Overview.tsx

Lines changed: 132 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function Overview({ liveStates }: OverviewProps) {
9292
{/* Agent Cards */}
9393
<section>
9494
<h2 className="mb-4 font-plex text-sm font-medium text-ink-dull">Agents</h2>
95-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
95+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-3">
9696
{agents.map((agent) => (
9797
<AgentCard
9898
key={agent.id}
@@ -209,6 +209,54 @@ function HeroSection({
209209
);
210210
}
211211

212+
/** Deterministic gradient from a seed string. Produces two HSL colors. */
213+
function seedGradient(seed: string): [string, string] {
214+
let hash = 0;
215+
for (let i = 0; i < seed.length; i++) {
216+
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
217+
hash |= 0;
218+
}
219+
const hue1 = ((hash >>> 0) % 360);
220+
const hue2 = (hue1 + 40 + ((hash >>> 8) % 60)) % 360;
221+
return [
222+
`hsl(${hue1}, 70%, 55%)`,
223+
`hsl(${hue2}, 60%, 45%)`,
224+
];
225+
}
226+
227+
function AgentAvatar({ seed, size = 64 }: { seed: string; size?: number }) {
228+
const [c1, c2] = seedGradient(seed);
229+
const gradientId = `avatar-${seed.replace(/[^a-z0-9]/gi, "")}`;
230+
return (
231+
<svg width={size} height={size} viewBox="0 0 64 64" className="flex-shrink-0 rounded-full">
232+
<defs>
233+
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="100%">
234+
<stop offset="0%" stopColor={c1} />
235+
<stop offset="100%" stopColor={c2} />
236+
</linearGradient>
237+
</defs>
238+
<circle cx="32" cy="32" r="32" fill={`url(#${gradientId})`} />
239+
</svg>
240+
);
241+
}
242+
243+
/** Gradient banner SVG that fills the card top. Uses the same seed as the avatar. */
244+
function BannerGradient({ seed }: { seed: string }) {
245+
const [c1, c2] = seedGradient(seed);
246+
const bannerId = `banner-${seed.replace(/[^a-z0-9]/gi, "")}`;
247+
return (
248+
<svg className="absolute inset-0 h-full w-full rounded-t-xl" preserveAspectRatio="none" viewBox="0 0 400 100">
249+
<defs>
250+
<linearGradient id={bannerId} x1="0%" y1="0%" x2="100%" y2="100%">
251+
<stop offset="0%" stopColor={c1} stopOpacity={0.25} />
252+
<stop offset="100%" stopColor={c2} stopOpacity={0.15} />
253+
</linearGradient>
254+
</defs>
255+
<rect width="400" height="100" fill={`url(#${bannerId})`} />
256+
</svg>
257+
);
258+
}
259+
212260
function AgentCard({
213261
agent,
214262
liveActivity,
@@ -217,72 +265,111 @@ function AgentCard({
217265
liveActivity: { workers: number; branches: number; hasActivity: boolean };
218266
}) {
219267
const isActive = liveActivity.hasActivity || (agent.last_activity_at && new Date(agent.last_activity_at).getTime() > Date.now() - 5 * 60 * 1000);
268+
const profile = agent.profile;
269+
const displayName = profile?.display_name ?? agent.id;
270+
const avatarSeed = profile?.avatar_seed ?? agent.id;
220271

221272
return (
222273
<Link
223274
to="/agents/$agentId"
224275
params={{ agentId: agent.id }}
225-
className="group flex flex-col rounded-xl border border-app-line bg-app-darkBox p-5 transition-all hover:border-app-line/80 hover:bg-app-darkBox/80"
276+
className="group flex flex-col rounded-xl border border-app-line bg-app-darkBox overflow-hidden transition-all hover:border-app-line/80 hover:bg-app-darkBox/80"
226277
>
227-
<div className="flex items-start justify-between">
228-
<div className="flex items-center gap-2">
229-
<div className={`h-2.5 w-2.5 rounded-full ${isActive ? "bg-green-500" : "bg-gray-500"}`} />
230-
<h3 className="font-plex text-lg font-medium text-ink">{agent.id}</h3>
231-
</div>
278+
{/* Banner + Avatar */}
279+
<div className="relative h-20">
280+
<BannerGradient seed={avatarSeed} />
281+
{/* Live activity badges — top right */}
282+
{(liveActivity.workers > 0 || liveActivity.branches > 0) && (
283+
<div className="absolute top-2.5 right-3 flex items-center gap-1.5 z-10">
284+
{liveActivity.workers > 0 && (
285+
<span className="flex items-center gap-1 rounded-full bg-amber-500/20 backdrop-blur-sm px-2 py-0.5 text-tiny text-amber-400">
286+
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
287+
{liveActivity.workers}w
288+
</span>
289+
)}
290+
{liveActivity.branches > 0 && (
291+
<span className="flex items-center gap-1 rounded-full bg-violet-500/20 backdrop-blur-sm px-2 py-0.5 text-tiny text-violet-400">
292+
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400" />
293+
{liveActivity.branches}b
294+
</span>
295+
)}
296+
</div>
297+
)}
232298
</div>
233299

234-
{/* Sparkline */}
235-
<div className="mt-3 h-10">
236-
<SparklineChart data={agent.activity_sparkline} />
300+
{/* Avatar — overlapping the banner */}
301+
<div className="relative px-5 -mt-8">
302+
<div className="relative inline-block">
303+
<div className="rounded-full border-[3px] border-app-darkBox">
304+
<AgentAvatar seed={avatarSeed} size={64} />
305+
</div>
306+
<div
307+
className={`absolute bottom-0.5 right-0.5 h-4 w-4 rounded-full border-[2.5px] border-app-darkBox ${
308+
isActive ? "bg-green-500" : "bg-gray-500"
309+
}`}
310+
/>
311+
</div>
237312
</div>
238313

239-
{/* Stats row */}
240-
<div className="mt-3 flex items-center gap-4 text-tiny">
241-
<div className="flex items-center gap-1">
242-
<span className="text-ink-faint">Channels</span>
243-
<span className="font-medium text-ink-dull">{agent.channel_count}</span>
244-
</div>
245-
<div className="flex items-center gap-1">
246-
<span className="text-ink-faint">Memories</span>
247-
<span className="font-medium text-ink-dull">{agent.memory_total.toLocaleString()}</span>
248-
</div>
249-
<div className="flex items-center gap-1">
250-
<span className="text-ink-faint">Cron</span>
251-
<span className="font-medium text-ink-dull">{agent.cron_job_count}</span>
252-
</div>
314+
{/* Name + status */}
315+
<div className="px-5 mt-2">
316+
<h3 className="font-plex text-xl font-semibold text-ink truncate">
317+
{displayName}
318+
</h3>
319+
{profile?.status ? (
320+
<p className="mt-1 text-sm text-ink-dull italic">
321+
{profile.status}
322+
</p>
323+
) : agent.last_activity_at ? (
324+
<p className="mt-1 text-tiny text-ink-faint">
325+
Active {formatTimeAgo(agent.last_activity_at)}
326+
</p>
327+
) : null}
253328
</div>
254329

255-
{/* Live activity badges */}
256-
{(liveActivity.workers > 0 || liveActivity.branches > 0) && (
257-
<div className="mt-3 flex flex-wrap gap-1.5">
258-
{liveActivity.workers > 0 && (
259-
<span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-tiny text-amber-400">
260-
{liveActivity.workers}w
261-
</span>
262-
)}
263-
{liveActivity.branches > 0 && (
264-
<span className="rounded-full bg-violet-500/10 px-2 py-0.5 text-tiny text-violet-400">
265-
{liveActivity.branches}b
266-
</span>
267-
)}
268-
</div>
330+
{/* Bio — full text, no truncation */}
331+
{profile?.bio && (
332+
<p className="px-5 mt-3 text-sm leading-relaxed text-ink-dull">
333+
{profile.bio}
334+
</p>
269335
)}
270336

271-
{/* Footer */}
272-
<div className="mt-3 flex items-center justify-between text-tiny">
273-
{agent.last_activity_at ? (
274-
<span className="text-ink-faint">Active {formatTimeAgo(agent.last_activity_at)}</span>
275-
) : (
276-
<span className="text-ink-faint">No activity</span>
337+
{/* Spacer pushes footer down when bio is short */}
338+
<div className="flex-1" />
339+
340+
{/* Sparkline */}
341+
<div className="mx-5 mt-4 h-10">
342+
<SparklineChart data={agent.activity_sparkline} />
343+
</div>
344+
345+
{/* Stats footer */}
346+
<div className="flex items-center gap-4 px-5 py-3.5 mt-3 border-t border-app-line/50 text-tiny">
347+
<StatPill label="channels" value={agent.channel_count} />
348+
<StatPill label="memories" value={agent.memory_total} />
349+
{agent.cron_job_count > 0 && (
350+
<StatPill label="cron" value={agent.cron_job_count} />
277351
)}
278352
{agent.last_bulletin_at && (
279-
<span className="text-accent/70">Bulletin {formatTimeAgo(agent.last_bulletin_at)}</span>
353+
<span className="ml-auto text-accent/60">
354+
Bulletin {formatTimeAgo(agent.last_bulletin_at)}
355+
</span>
280356
)}
281357
</div>
282358
</Link>
283359
);
284360
}
285361

362+
function StatPill({ label, value }: { label: string; value: number }) {
363+
return (
364+
<span className="text-ink-faint">
365+
<span className="font-medium tabular-nums text-ink-dull">
366+
{value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value}
367+
</span>
368+
{" "}{label}
369+
</span>
370+
);
371+
}
372+
286373
const CHART_COLORS = {
287374
accent: "#6366f1",
288375
accentBg: "#1e1b4b",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Agent profile: cortex-generated personality data displayed on the overview card.
2+
-- One row per agent, upserted each time the cortex regenerates.
3+
CREATE TABLE IF NOT EXISTS agent_profile (
4+
agent_id TEXT PRIMARY KEY NOT NULL,
5+
display_name TEXT, -- optional human-friendly name the cortex picks
6+
status TEXT, -- short mood/status line (e.g. "deep in a rewrite")
7+
bio TEXT, -- 2-3 sentence self-description
8+
avatar_seed TEXT, -- seed string for deterministic gradient generation
9+
generated_at TEXT NOT NULL DEFAULT (datetime('now')),
10+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
11+
);

prompts/en/cortex_profile.md.j2

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
You are generating a profile card for an AI agent. You have access to the agent's memory bulletin (a synthesis of its knowledge, recent activity, and identity) and its identity files.
2+
3+
Based on this context, generate a JSON object with exactly these fields:
4+
5+
- **display_name**: A short name or alias for the agent. If the identity files define a name, use it. Otherwise, invent something that fits the agent's personality. 1-3 words max.
6+
- **status**: A short, casual status line reflecting what the agent has been up to or how it's "feeling" based on recent activity and memories. Think Discord status — brief, personality-driven, sometimes witty. 3-10 words. Examples: "deep in a refactor", "learning about your codebase", "just vibing between tasks", "drowning in PRs".
7+
- **bio**: A 2-3 sentence self-description written in first person. This is the agent introducing itself. It should reflect personality from the identity/soul files and reference real things from its memories (projects it's worked on, things it knows about, its role). Keep it natural and conversational.
8+
9+
Respond with ONLY the raw JSON object. No markdown fencing, no explanation.
10+
11+
Example output:
12+
{"display_name": "Atlas", "status": "knee-deep in the auth rewrite", "bio": "I'm the dev agent for the Spacebot project. I've been spending most of my time on the Rust backend — memory systems, API endpoints, and keeping the cortex running smoothly. I know my way around James's codebase better than most humans at this point."}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Generate the agent profile based on the following context.
2+
3+
{% if identity_context %}
4+
## Identity Files
5+
6+
{{ identity_context }}
7+
{% endif %}
8+
9+
{% if memory_bulletin %}
10+
## Current Memory Bulletin
11+
12+
{{ memory_bulletin }}
13+
{% endif %}

0 commit comments

Comments
 (0)