Skip to content
Merged
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
45 changes: 28 additions & 17 deletions .github/workflows/deploy-langgraph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,33 @@ jobs:
strategy:
matrix:
include:
- name: langgraph-streaming
- name: streaming
path: cockpit/langgraph/streaming/python
- name: langgraph-persistence
- name: persistence
path: cockpit/langgraph/persistence/python
- name: langgraph-interrupts
- name: interrupts
path: cockpit/langgraph/interrupts/python
- name: langgraph-memory
- name: memory
path: cockpit/langgraph/memory/python
- name: langgraph-durable-execution
- name: durable-execution
path: cockpit/langgraph/durable-execution/python
- name: langgraph-subgraphs
- name: subgraphs
path: cockpit/langgraph/subgraphs/python
- name: langgraph-time-travel
- name: time-travel
path: cockpit/langgraph/time-travel/python
- name: langgraph-deployment-runtime
- name: deployment-runtime
path: cockpit/langgraph/deployment-runtime/python
- name: deep-agents-planning
- name: planning
path: cockpit/deep-agents/planning/python
- name: deep-agents-filesystem
- name: filesystem
path: cockpit/deep-agents/filesystem/python
- name: deep-agents-subagents
- name: da-subagents
path: cockpit/deep-agents/subagents/python
- name: deep-agents-memory
- name: da-memory
path: cockpit/deep-agents/memory/python
- name: deep-agents-skills
- name: skills
path: cockpit/deep-agents/skills/python
- name: deep-agents-sandboxes
- name: sandboxes
path: cockpit/deep-agents/sandboxes/python
steps:
- uses: actions/checkout@v6.0.2
Expand All @@ -54,14 +54,25 @@ jobs:
with:
python-version: '3.12'

- name: Install langgraph-cli
run: pip install langgraph-cli
- name: Install uv
run: pip install uv

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Write .env for deployment
if: |
github.event_name == 'workflow_dispatch' && (inputs.capability == '' || contains(matrix.path, inputs.capability))
|| github.event_name == 'push'
working-directory: ${{ matrix.path }}
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" > .env

- name: Deploy ${{ matrix.name }}
if: |
github.event_name == 'workflow_dispatch' && (inputs.capability == '' || contains(matrix.path, inputs.capability))
|| github.event_name == 'push'
working-directory: ${{ matrix.path }}
run: langgraph deploy
run: uv run --with langgraph-cli langgraph deploy --name ${{ matrix.name }} --no-wait
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
28 changes: 7 additions & 21 deletions apps/cockpit/e2e/cockpit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,36 @@
import { expect, test } from '@playwright/test';

test('renders navigation and representative shell panes on the home page', async ({ page }) => {
test('renders navigation and shell on the home page', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('main', { name: 'Cockpit shell' })).toHaveAttribute(
'data-hydrated',
'true'
);

await expect(page.getByRole('heading', { name: 'Explore the example surface' })).toBeVisible();
await expect(page.getByRole('navigation', { name: 'Cockpit navigation' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Run', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'Code', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'Docs', exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Interactive example' })).toBeVisible();
await expect(page.getByText('apps/cockpit/src/app/page.tsx')).toBeVisible();
});

test('navigates from the tree to a capability route and shows the loaded surface', async ({ page }) => {
test('navigates from the sidebar to a capability route', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('main', { name: 'Cockpit shell' })).toHaveAttribute(
'data-hydrated',
'true'
);

await page.getByRole('link', { name: 'LangGraph Persistence' }).click();
// Sidebar strips "LangGraph " prefix, so the link text is just "Persistence"
await page.getByRole('link', { name: 'Persistence', exact: true }).click();

await expect(page).toHaveURL(/\/langgraph\/core-capabilities\/persistence\/overview\/python$/);
await expect(page.getByRole('main', { name: 'Cockpit shell' })).toHaveAttribute(
'data-hydrated',
'true'
);
await expect(
page.getByText('/docs/langgraph/core-capabilities/persistence/overview/python')
).toBeVisible();
await page.getByRole('button', { name: 'Code', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Code' })).toBeVisible();
await page.getByRole('tab', { name: 'index.ts' }).click();
await expect(
page.getByText('cockpit/langgraph/persistence/python/src/index.ts', { exact: true })
).toBeVisible();
await page.getByRole('button', { name: 'Open prompt assets' }).click();
await expect(page.getByRole('complementary', { name: 'Prompt drawer' })).toBeVisible();
await expect(
page.getByText('cockpit/langgraph/persistence/python/prompts/persistence.md')
).toBeVisible();

// Mode switcher should still be present
await expect(page.getByRole('button', { name: 'Code', exact: true })).toBeVisible();
});

test('falls back to the product overview when a missing typescript route is requested', async ({ page }) => {
Expand All @@ -54,5 +41,4 @@ test('falls back to the product overview when a missing typescript route is requ
'data-hydrated',
'true'
);
await expect(page.getByText('/docs/langgraph/getting-started/overview/overview/python')).toBeVisible();
});
2 changes: 1 addition & 1 deletion apps/cockpit/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const shouldStartLocalServer = !process.env['BASE_URL'];

export default defineConfig({
testDir: './e2e',
testIgnore: ['**/all-examples-smoke*'],
testIgnore: ['**/all-examples-smoke*', '**/production-smoke*'],
fullyParallel: true,
use: {
baseURL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,58 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component, computed } from '@angular/core';
import { ChatDebugComponent } from '@cacheplane/chat';
import { LegacyChatComponent } from '@cacheplane/chat';
import { streamResource } from '@cacheplane/stream-resource';
import { AIMessage } from '@langchain/core/messages';
import { environment } from '../environments/environment';

interface ToolCallEntry {
name: string;
args: string;
result?: string;
}

/**
* FilesystemComponent demonstrates agent file operations.
*
* The agent can read and write files using tool calls. The sidebar
* shows a real-time log of each file operation as it happens.
*
* Key integration points:
* - `stream.messages()` contains all messages including tool call results
* - `computed()` derives tool call entries from AI messages
* - Tool calls update reactively as the agent performs file operations
*/
@Component({
selector: 'app-filesystem',
standalone: true,
imports: [ChatDebugComponent],
imports: [LegacyChatComponent],
template: `
<div class="flex h-screen">
<chat-debug [ref]="stream" class="flex-1 min-w-0" />
<aside class="w-72 shrink-0 border-l overflow-y-auto p-4 space-y-2"
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717); color: var(--chat-text, #e0e0e0);">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-3"
style="color: var(--chat-text-muted, #777);">File Operations</h3>
@if (fileOps().length === 0) {
<p class="text-sm italic" style="color: var(--chat-text-muted, #777);">No file operations yet. Ask the agent to read or write a file.</p>
}
@for (op of fileOps(); track $index) {
<div class="flex items-start gap-2 text-sm py-1">
<span class="shrink-0">{{ op.name === 'read_file' ? '📖' : '✏️' }}</span>
<span class="font-mono text-xs break-all"
style="color: var(--chat-text, #e0e0e0);">{{ op.path }}</span>
<cp-chat
[messages]="stream.messages()"
[isLoading]="stream.isLoading()"
[error]="stream.error()"
(sendMessage)="send($event)">
<ng-template #sidebar>
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">File Operations</h3>
@for (entry of toolCallEntries(); track $index) {
<div style="display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; font-size: 0.8rem; border-bottom: 1px solid #e5e7eb;">
<span style="flex-shrink: 0; font-size: 1rem; line-height: 1.2;">
{{ entry.name === 'read_file' ? '📖' : '✏️' }}
</span>
<div style="min-width: 0;">
<div style="font-weight: 500; color: #1a1a2e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ getFilePath(entry.args) }}
</div>
<div style="color: #8b8fa3; font-size: 0.75rem; margin-top: 2px;">
{{ entry.name === 'read_file' ? 'read' : 'write' }}
{{ entry.result ? ' · done' : ' · running…' }}
</div>
</div>
</div>
}
</aside>
</div>
@empty {
<p style="color: #8b8fa3; font-size: 0.8rem;">Ask the agent to read or write a file.</p>
}
</ng-template>
</cp-chat>
`,
})
export class FilesystemComponent {
Expand All @@ -36,17 +61,29 @@ export class FilesystemComponent {
assistantId: environment.streamingAssistantId,
});

protected readonly fileOps = computed(() => {
const messages = this.stream.messages();
const ops: { name: string; path: string }[] = [];
for (const msg of messages) {
if (!(msg instanceof AIMessage)) continue;
for (const tc of this.stream.getToolCalls(msg)) {
if (tc.call.name === 'read_file' || tc.call.name === 'write_file') {
ops.push({ name: tc.call.name, path: (tc.call.args as Record<string, string>)?.['path'] ?? '' });
toolCallEntries = computed(() => {
const msg = this.stream.messages();
const calls: ToolCallEntry[] = [];
for (const m of msg) {
if ((m as any).tool_calls) {
for (const tc of (m as any).tool_calls) {
calls.push({ name: tc.name, args: JSON.stringify(tc.args), result: tc.output });
}
}
}
return ops;
return calls;
});

getFilePath(args: string): string {
try {
const parsed = JSON.parse(args);
return parsed.path ?? args;
} catch {
return args;
}
}

send(text: string): void {
this.stream.submit({ messages: [{ role: 'human', content: text }] });
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* Production environment configuration.
*
* Points to the LangGraph Cloud deployment managed by LangSmith.
* The assistantId must match the graph name in langgraph.json.
* Uses relative /api URL — Vercel middleware proxies to LangGraph Cloud
* and injects the x-api-key header server-side.
*/
export const environment = {
production: true,
langGraphApiUrl: 'https://filesystem-2330285f57625bff8654bc026f70a6ae.us.langgraph.app',
langGraphApiUrl: '/api',
streamingAssistantId: 'filesystem',
};
7 changes: 5 additions & 2 deletions cockpit/deep-agents/filesystem/python/langgraph.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"graphs": {
"filesystem": "./src/graph.py:graph"
},
"dependencies": ["."],
"python_version": "3.12"
"dependencies": [
"."
],
"python_version": "3.12",
"env": ".env"
}
60 changes: 37 additions & 23 deletions cockpit/deep-agents/memory/angular/src/app/memory.component.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component, computed } from '@angular/core';
import { ChatDebugComponent } from '@cacheplane/chat';
import { LegacyChatComponent } from '@cacheplane/chat';
import { streamResource } from '@cacheplane/stream-resource';
import { environment } from '../environments/environment';

/**
* MemoryComponent demonstrates persistent agent memory across sessions.
*
* The agent extracts facts about the user from each conversation turn
* and stores them in `agent_memory` state. The sidebar shows all learned
* facts in real time as the agent updates its memory.
*
* Key integration points:
* - `stream.value()` contains the agent state including `agent_memory`
* - `computed()` derives key/value pairs for the sidebar
* - Memory entries update reactively as the agent learns new facts
*/
@Component({
selector: 'app-da-memory',
standalone: true,
imports: [ChatDebugComponent],
imports: [LegacyChatComponent],
template: `
<div class="flex h-screen">
<chat-debug [ref]="stream" class="flex-1 min-w-0" />
<aside class="w-72 shrink-0 border-l overflow-y-auto p-4 space-y-2"
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717); color: var(--chat-text, #e0e0e0);">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-3"
style="color: var(--chat-text-muted, #777);">Agent Memory</h3>
@if (memoryEntries().length === 0) {
<p class="text-sm italic" style="color: var(--chat-text-muted, #777);">No facts learned yet. Have a conversation to build memory.</p>
}
@for (entry of memoryEntries(); track $index) {
<div class="text-sm py-1">
<span class="font-semibold" style="color: var(--chat-text-muted, #777);">{{ entry[0] }}:</span>
<span class="ml-1" style="color: var(--chat-text, #e0e0e0);">{{ entry[1] }}</span>
<cp-chat
[messages]="stream.messages()"
[isLoading]="stream.isLoading()"
[error]="stream.error()"
(sendMessage)="send($event)">
<ng-template #sidebar>
<h3 style="font-size: 0.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #1a1a2e;">Learned Facts</h3>
@for (entry of memoryEntries(); track entry[0]) {
<div style="padding: 6px 0; font-size: 0.8rem; border-bottom: 1px solid #e5e7eb;">
<div style="font-weight: 600; color: #1a1a2e; margin-bottom: 2px;">{{ entry[0] }}</div>
<div style="color: #555770;">{{ entry[1] }}</div>
</div>
}
</aside>
</div>
@empty {
<p style="color: #8b8fa3; font-size: 0.8rem;">Tell the agent something about yourself to see it remember.</p>
}
</ng-template>
</cp-chat>
`,
})
export class MemoryComponent {
Expand All @@ -34,10 +46,12 @@ export class MemoryComponent {
assistantId: environment.streamingAssistantId,
});

protected readonly memoryEntries = computed(() => {
const val = this.stream.value() as Record<string, unknown>;
const mem = val?.['agent_memory'];
if (!mem || typeof mem !== 'object') return [];
return Object.entries(mem as Record<string, string>);
memoryEntries = computed(() => {
const val = this.stream.value() as { agent_memory?: Record<string, string> } | undefined;
return Object.entries(val?.agent_memory ?? {});
});

send(text: string): void {
this.stream.submit({ messages: [{ role: 'human', content: text }] });
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* Production environment configuration.
*
* Points to the LangGraph Cloud deployment managed by LangSmith.
* The assistantId must match the graph name in langgraph.json.
* Uses relative /api URL — Vercel middleware proxies to LangGraph Cloud
* and injects the x-api-key header server-side.
*/
export const environment = {
production: true,
langGraphApiUrl: 'https://da-memory-15f767adfa6f5cd48bd45a0fa4db29b5.us.langgraph.app',
langGraphApiUrl: '/api',
streamingAssistantId: 'da-memory',
};
Loading
Loading