Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.apple.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ services:
- ./.env
environment:
- LIVEKIT_URL=ws://livekit:7880
- CAAL_MEMORY_DIR=/app/data
# Point to mlx-audio (single service for STT + TTS)
- SPEACHES_URL=${MLX_AUDIO_URL:-http://host.docker.internal:8001}
- KOKORO_URL=${MLX_AUDIO_URL:-http://host.docker.internal:8001}
Expand Down
1 change: 1 addition & 0 deletions docker-compose.cpu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ services:
environment:
- LIVEKIT_URL=ws://livekit:7880
- SPEACHES_URL=http://speaches:8000
- CAAL_MEMORY_DIR=/app/data
volumes:
- caal-memory:/app/data
- caal-config:/app/config # Runtime config (settings.json, mcp_servers.json)
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ services:
- LIVEKIT_URL=ws://livekit:7880
- SPEACHES_URL=http://speaches:8000
- KOKORO_URL=http://kokoro:8880
- CAAL_MEMORY_DIR=/app/data
volumes:
- caal-memory:/app/data
- caal-config:/app/config # Runtime config (settings.json, mcp_servers.json)
Expand Down
12 changes: 12 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
set -e

CONFIG_DIR="/app/config"
DATA_DIR="/app/data"

# Ensure config directory exists and is writable by agent
mkdir -p "$CONFIG_DIR"
chown agent:agent "$CONFIG_DIR"

# Ensure data directory exists and is writable by agent (memory persistence)
mkdir -p "$DATA_DIR"
chown agent:agent "$DATA_DIR"

# settings.json - copy default if missing
if [ ! -f "$CONFIG_DIR/settings.json" ]; then
echo "Creating settings.json from defaults..."
Expand All @@ -24,9 +29,16 @@ if [ ! -f "$CONFIG_DIR/mcp_servers.json" ]; then
chown agent:agent "$CONFIG_DIR/mcp_servers.json"
fi

# registry_cache.json - create empty if missing
if [ ! -f "$CONFIG_DIR/registry_cache.json" ]; then
echo '{}' > "$CONFIG_DIR/registry_cache.json"
chown agent:agent "$CONFIG_DIR/registry_cache.json"
fi

# Create symlinks from /app to config files (for code that expects them in /app)
ln -sf "$CONFIG_DIR/settings.json" /app/settings.json
ln -sf "$CONFIG_DIR/mcp_servers.json" /app/mcp_servers.json
ln -sf "$CONFIG_DIR/registry_cache.json" /app/registry_cache.json

# Drop privileges and execute the main command as agent user
exec gosu agent "$@"
63 changes: 63 additions & 0 deletions frontend/app/api/memory/[key]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server';

const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://agent:8889';

interface RouteParams {
params: Promise<{ key: string }>;
}

/**
* GET /api/memory/[key] - Get a single memory entry
*/
export async function GET(_request: NextRequest, { params }: RouteParams) {
try {
const { key } = await params;
const res = await fetch(`${WEBHOOK_URL}/memory/${encodeURIComponent(key)}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});

if (!res.ok) {
const text = await res.text();
console.error(`[/api/memory/${key}] Backend error:`, res.status, text);
return NextResponse.json({ error: text || 'Backend error' }, { status: res.status });
}

const data = await res.json();
return NextResponse.json(data);
} catch (error) {
console.error('[/api/memory/[key]] Error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

/**
* DELETE /api/memory/[key] - Delete a single memory entry
*/
export async function DELETE(_request: NextRequest, { params }: RouteParams) {
try {
const { key } = await params;
const res = await fetch(`${WEBHOOK_URL}/memory/${encodeURIComponent(key)}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});

if (!res.ok) {
const text = await res.text();
console.error(`[/api/memory/${key}] Backend error:`, res.status, text);
return NextResponse.json({ error: text || 'Backend error' }, { status: res.status });
}

const data = await res.json();
return NextResponse.json(data);
} catch (error) {
console.error('[/api/memory/[key]] Error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}
87 changes: 87 additions & 0 deletions frontend/app/api/memory/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from 'next/server';

const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://agent:8889';

/**
* GET /api/memory - List all memory entries
*/
export async function GET() {
try {
const res = await fetch(`${WEBHOOK_URL}/memory`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});

if (!res.ok) {
const text = await res.text();
console.error('[/api/memory] Backend error:', res.status, text);
return NextResponse.json({ error: text || 'Backend error' }, { status: res.status });
}

const data = await res.json();
return NextResponse.json(data);
} catch (error) {
console.error('[/api/memory] Error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

/**
* POST /api/memory - Store a new memory entry
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();

const res = await fetch(`${WEBHOOK_URL}/memory`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});

if (!res.ok) {
const text = await res.text();
console.error('[/api/memory] Backend error:', res.status, text);
return NextResponse.json({ error: text || 'Backend error' }, { status: res.status });
}

const data = await res.json();
return NextResponse.json(data);
} catch (error) {
console.error('[/api/memory] Error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}

/**
* DELETE /api/memory - Clear all memory entries
*/
export async function DELETE() {
try {
const res = await fetch(`${WEBHOOK_URL}/memory`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});

if (!res.ok) {
const text = await res.text();
console.error('[/api/memory] Backend error:', res.status, text);
return NextResponse.json({ error: text || 'Backend error' }, { status: res.status });
}

const data = await res.json();
return NextResponse.json(data);
} catch (error) {
console.error('[/api/memory] Error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}
6 changes: 6 additions & 0 deletions frontend/components/app/view-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSessionContext } from '@livekit/components-react';
import type { AppConfig } from '@/app-config';
import { SessionView } from '@/components/app/session-view';
import { WelcomeView } from '@/components/app/welcome-view';
import { MemoryPanel } from '@/components/memory';
import { SettingsPanel } from '@/components/settings/settings-panel';
import { ToolsPanel } from '@/components/tools';

Expand Down Expand Up @@ -38,6 +39,7 @@ export function ViewController({ appConfig }: ViewControllerProps) {
const { isConnected, start } = useSessionContext();
const [settingsOpen, setSettingsOpen] = useState(false);
const [toolsOpen, setToolsOpen] = useState(false);
const [memoryOpen, setMemoryOpen] = useState(false);

return (
<>
Expand All @@ -50,6 +52,7 @@ export function ViewController({ appConfig }: ViewControllerProps) {
onStartCall={start}
onOpenSettings={() => setSettingsOpen(true)}
onOpenTools={() => setToolsOpen(true)}
onOpenMemory={() => setMemoryOpen(true)}
/>
)}
{/* Session view */}
Expand All @@ -63,6 +66,9 @@ export function ViewController({ appConfig }: ViewControllerProps) {

{/* Tools panel */}
<ToolsPanel isOpen={toolsOpen} onClose={() => setToolsOpen(false)} />

{/* Memory panel */}
<MemoryPanel isOpen={memoryOpen} onClose={() => setMemoryOpen(false)} />
</>
);
}
13 changes: 12 additions & 1 deletion frontend/components/app/welcome-view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useTranslations } from 'next-intl';
import { Gear, Wrench } from '@phosphor-icons/react/dist/ssr';
import { Brain, Gear, Wrench } from '@phosphor-icons/react/dist/ssr';
import { Button } from '@/components/livekit/button';

function WelcomeImage() {
Expand All @@ -26,12 +26,14 @@ interface WelcomeViewProps {
onStartCall: () => void;
onOpenSettings?: () => void;
onOpenTools?: () => void;
onOpenMemory?: () => void;
}

export const WelcomeView = ({
onStartCall,
onOpenSettings,
onOpenTools,
onOpenMemory,
ref,
}: React.ComponentProps<'div'> & WelcomeViewProps) => {
const t = useTranslations('Welcome');
Expand All @@ -41,6 +43,15 @@ export const WelcomeView = ({
<div ref={ref} className="relative min-h-screen" style={{ background: 'var(--surface-deep)' }}>
{/* Top right buttons */}
<div className="fixed top-6 right-6 z-40 flex items-center gap-2">
{onOpenMemory && (
<button
onClick={onOpenMemory}
className="text-muted-foreground hover:text-foreground hover:bg-muted rounded-full p-2 transition-colors"
title={tCommon('memory')}
>
<Brain className="h-6 w-6" weight="fill" />
</button>
)}
{onOpenTools && (
<button
onClick={onOpenTools}
Expand Down
1 change: 1 addition & 0 deletions frontend/components/memory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MemoryPanel } from './memory-panel';
Loading