diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs-v2/getting-started/introduction.mdx index 3aeae6115..eddc6d57d 100644 --- a/apps/website/content/docs-v2/getting-started/introduction.mdx +++ b/apps/website/content/docs-v2/getting-started/introduction.mdx @@ -27,29 +27,16 @@ No RxJS. No manual subscriptions. No async pipes. Just Signals that work with An ## The Architecture -StreamResource sits between your Angular app and LangGraph Platform: +Watch a full conversation turn flow through the stack — from user input to rendered response: - - -Creates a reactive resource bound to a specific agent. All state is exposed as Signals. - - -Sends HTTP POST to LangGraph Platform, receives Server-Sent Events with state updates. - - -Executes nodes, calls tools, manages checkpoints. Streams results back in real-time. - - -As tokens arrive, `messages()` updates. Angular re-renders only the affected components. - - + ## Build Your Agent LangGraph agents are Python programs defined as directed graphs. Here's a minimal chat agent using the example from this repository: - - + + ```python # examples/chat-agent/src/chat_agent/agent.py @@ -58,7 +45,7 @@ from langchain_core.runnables import RunnableConfig from langgraph.graph import END, START, MessagesState, StateGraph from langchain_openai import ChatOpenAI -llm = ChatOpenAI(model="gpt-4o-mini") +llm = ChatOpenAI(model="gpt-5-mini") def call_model(state: MessagesState, config: RunnableConfig) -> dict: """Invoke the LLM with the current message history.""" @@ -79,7 +66,7 @@ graph = builder.compile() ``` - + ```json { @@ -162,8 +149,8 @@ export const appConfig: ApplicationConfig = { - - + + ```typescript // chat.component.ts @@ -202,7 +189,7 @@ export class ChatComponent { ``` - + ```html @@ -257,46 +244,91 @@ Open `http://localhost:4200` and start chatting with your agent. Messages stream Here's what streamResource() gives you out of the box: -| Feature | Signal | Description | -|---------|--------|-------------| -| **Messages** | `chat.messages()` | Live message list, updates as tokens arrive | -| **Status** | `chat.status()` | Current state: idle, loading, resolved, error | -| **Thread persistence** | `threadId` option | Conversations survive page refreshes | -| **Interrupts** | `chat.interrupt()` | Agent pauses for human input | -| **History** | `chat.history()` | Full checkpoint timeline for time-travel | -| **Subagents** | `chat.subagents()` | Track delegated agent work | -| **Tool calls** | `chat.toolCalls()` | See what tools the agent is using | + + + `chat.messages()` — live message list that updates as each token arrives from the agent + + + `chat.status()` — current state: idle, loading, resolved, or error + + + `threadId` option — conversations survive page refreshes via localStorage or backend + + + `chat.interrupt()` — agent pauses for human approval, your UI handles the decision + + + `chat.history()` — full checkpoint timeline for debugging and branching + + + `chat.subagents()` — track delegated agent work across multiple graphs + + + `chat.toolCalls()` — see what tools the agent is invoking in real-time + + + `MockStreamTransport` — deterministic testing without a running server + + ## Deploy to Production -When you're ready to go live, deploy your agent to LangGraph Cloud. +When you're ready to go live, deploy your agent to LangGraph Cloud and point your Angular app to the deployment URL. -Your agent code (the Python project with `langgraph.json`) needs to be in a GitHub repository. +Your agent code (the Python project with `langgraph.json`) needs to be in a GitHub repository. Make sure your `langgraph.json` references the correct graph entry point. + +```bash +git init && git add . && git commit -m "initial agent" +gh repo create my-agent --public --source=. --push +``` -Go to [LangSmith Deployments](https://smith.langchain.com) and click **+ New Deployment**. Connect your GitHub repo and deploy. This takes about 15 minutes. +Go to [LangSmith Deployments](https://smith.langchain.com) and click **+ New Deployment**. Connect your GitHub account, select your repository, and deploy. The first deployment takes about 15 minutes. + +You'll receive a deployment URL like `https://my-agent-abc123.langsmith.dev`. -Point `apiUrl` to your deployment URL: +Point `apiUrl` to your deployment URL and set up environment-based configuration: ```typescript +// environment.ts +export const environment = { + langgraphUrl: 'http://localhost:2024', // dev +}; + +// environment.prod.ts +export const environment = { + langgraphUrl: 'https://my-agent-abc123.langsmith.dev', // prod +}; + +// app.config.ts provideStreamResource({ - apiUrl: 'https://your-deployment.langsmith.dev', + apiUrl: environment.langgraphUrl, }) ``` + + + +Deploy your Angular frontend to any hosting platform — Vercel, Netlify, AWS, or your own infrastructure. Since streamResource() is a stateless client, your frontend has no server-side state requirements. + +```bash +ng build --configuration production +# Deploy dist/ to your hosting platform +``` + - -Your Angular app is a stateless client. All agent state lives on LangGraph Platform. This means you can deploy your Angular app anywhere — CDN, edge, SSR — without state management concerns. + +Your Angular app is a stateless client. All agent state — threads, checkpoints, memory — lives on LangGraph Platform. This means you can deploy your frontend anywhere (CDN, edge, SSR) without state management concerns. Scale your frontend independently of your agent infrastructure. ## What's Next diff --git a/apps/website/src/components/docs/ArchFlowDiagram.tsx b/apps/website/src/components/docs/ArchFlowDiagram.tsx new file mode 100644 index 000000000..bef621fd7 --- /dev/null +++ b/apps/website/src/components/docs/ArchFlowDiagram.tsx @@ -0,0 +1,169 @@ +'use client'; +import { useState, useEffect, useRef } from 'react'; +import { tokens } from '../../../lib/design-tokens'; + +interface LogEntry { + time: string; + source: 'angular' | 'transport' | 'langgraph' | 'signal'; + message: string; +} + +const SCENARIO: { delay: number; chatBubble?: { role: 'user' | 'assistant'; text: string; streaming?: boolean }; log: LogEntry }[] = [ + { delay: 0, chatBubble: { role: 'user', text: 'How do Angular Signals work with streaming?' }, log: { time: '0.00s', source: 'angular', message: 'chat.submit({ messages: [userMsg] })' } }, + { delay: 800, log: { time: '0.02s', source: 'transport', message: 'POST /threads/t_8f3a/runs/stream → 200' } }, + { delay: 1200, log: { time: '0.04s', source: 'langgraph', message: 'Executing node: call_model (gpt-5-mini)' } }, + { delay: 2200, log: { time: '0.82s', source: 'langgraph', message: 'SSE event: { type: "values", messages: [...] }' } }, + { delay: 2600, log: { time: '0.84s', source: 'transport', message: 'Received chunk → messages$.next([...])' } }, + { delay: 2800, log: { time: '0.85s', source: 'signal', message: 'messages() updated → 2 messages' } }, + { delay: 3000, chatBubble: { role: 'assistant', text: 'Angular Signals', streaming: true }, log: { time: '0.86s', source: 'signal', message: 'status() → "loading"' } }, + { delay: 3400, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous', streaming: true }, log: { time: '1.12s', source: 'transport', message: 'Received chunk → values event' } }, + { delay: 3900, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous, reactive way to', streaming: true }, log: { time: '1.45s', source: 'signal', message: 'messages() updated → streaming token' } }, + { delay: 4500, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous, reactive way to track streaming state.', streaming: true }, log: { time: '1.82s', source: 'langgraph', message: 'SSE event: { type: "values", status: "done" }' } }, + { delay: 5200, chatBubble: { role: 'assistant', text: 'Angular Signals provide a synchronous, reactive way to track streaming state. Each token updates the Signal, and OnPush change detection re-renders automatically.' }, log: { time: '2.10s', source: 'signal', message: 'status() → "resolved" ✓' } }, + { delay: 6000, log: { time: '2.12s', source: 'angular', message: 'Template re-rendered (OnPush) — 1 component' } }, +]; + +const SOURCE_COLORS: Record = { + angular: { bg: 'rgba(221,0,49,0.08)', text: '#c62828', label: 'ANGULAR' }, + transport: { bg: 'rgba(100,80,200,0.08)', text: '#5e35b1', label: 'TRANSPORT' }, + langgraph: { bg: 'rgba(0,64,144,0.08)', text: '#004090', label: 'LANGGRAPH' }, + signal: { bg: 'rgba(16,185,129,0.08)', text: '#059669', label: 'SIGNAL' }, +}; + +export function ArchFlowDiagram() { + const [logs, setLogs] = useState([]); + const [bubbles, setBubbles] = useState<{ role: 'user' | 'assistant'; text: string; streaming?: boolean }[]>([]); + const [cycle, setCycle] = useState(0); + const logRef = useRef(null); + + useEffect(() => { + const timeouts: ReturnType[] = []; + + const runScenario = () => { + setLogs([]); + setBubbles([]); + + SCENARIO.forEach((step, i) => { + timeouts.push(setTimeout(() => { + setLogs(prev => [...prev, step.log]); + if (step.chatBubble) { + setBubbles(prev => { + const existing = prev.findIndex(b => b.role === step.chatBubble!.role && b.role === 'assistant'); + if (existing >= 0 && step.chatBubble!.role === 'assistant') { + const updated = [...prev]; + updated[existing] = step.chatBubble!; + return updated; + } + return [...prev, step.chatBubble!]; + }); + } + if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; + }, step.delay)); + }); + + // Restart after completion + timeouts.push(setTimeout(() => { + setCycle(c => c + 1); + }, 8000)); + }; + + runScenario(); + return () => timeouts.forEach(clearTimeout); + }, [cycle]); + + return ( +
+ {/* Header bar */} +
+
+
+
+
+
+ streamResource() — live architecture flow + localhost:4200 +
+ +
+ {/* Left: Chat simulation */} +
+
Chat Interface
+ +
+ {bubbles.map((b, i) => ( +
+ {b.role === 'assistant' && ( +
AI
+ )} +
+ {b.text} + {b.streaming && } +
+
+ ))} +
+
+ + {/* Right: Console log */} +
+
Developer Console
+ + {logs.map((log, i) => { + const sc = SOURCE_COLORS[log.source]; + return ( +
+ {log.time} + {sc.label} + {log.message} +
+ ); + })} + + {logs.length === 0 && ( +
Waiting for interaction...
+ )} +
+
+ + +
+ ); +} diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index d1f6001c8..aa67747eb 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -5,6 +5,8 @@ import { Steps, Step } from './mdx/Steps'; import { Tabs, Tab } from './mdx/Tabs'; import { Card, CardGroup } from './mdx/Card'; import { CodeGroup } from './mdx/CodeGroup'; +import { Pre } from './mdx/CodeBlock'; +import { ArchFlowDiagram } from './ArchFlowDiagram'; import { DocsBreadcrumb } from './DocsBreadcrumb'; import { DocsPrevNext } from './DocsPrevNext'; import rehypePrettyCode from 'rehype-pretty-code'; @@ -19,6 +21,8 @@ const mdxComponents = { Card, CardGroup, CodeGroup, + ArchFlowDiagram, + pre: Pre, }; const rehypeOptions = { diff --git a/apps/website/src/components/docs/mdx/CodeBlock.tsx b/apps/website/src/components/docs/mdx/CodeBlock.tsx new file mode 100644 index 000000000..6b4d10647 --- /dev/null +++ b/apps/website/src/components/docs/mdx/CodeBlock.tsx @@ -0,0 +1,42 @@ +'use client'; +import { useRef, useState } from 'react'; + +export function Pre({ children, ...props }: React.HTMLAttributes) { + const ref = useRef(null); + const [copied, setCopied] = useState(false); + + const copy = async () => { + const text = ref.current?.textContent ?? ''; + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
{children}
+ +
+ ); +} diff --git a/apps/website/src/components/docs/mdx/Tabs.tsx b/apps/website/src/components/docs/mdx/Tabs.tsx index 772f8756d..89fac6851 100644 --- a/apps/website/src/components/docs/mdx/Tabs.tsx +++ b/apps/website/src/components/docs/mdx/Tabs.tsx @@ -1,11 +1,23 @@ 'use client'; -import { useState, Children } from 'react'; +import { useState, Children, isValidElement } from 'react'; import { tokens } from '../../../../lib/design-tokens'; +interface TabProps { + label?: string; + children: React.ReactNode; +} + export function Tabs({ items, children }: { items?: string[]; children: React.ReactNode }) { const [active, setActive] = useState(0); const tabs = Children.toArray(children); - const labels = items ?? tabs.map((_, i) => `Tab ${i + 1}`); + + // Extract labels: from items prop, from Tab label prop, or fallback + const labels = items ?? tabs.map((child, i) => { + if (isValidElement(child) && child.props.label) { + return child.props.label; + } + return `Tab ${i + 1}`; + }); return (
@@ -40,6 +52,6 @@ export function Tabs({ items, children }: { items?: string[]; children: React.Re ); } -export function Tab({ children }: { children: React.ReactNode }) { +export function Tab({ children }: TabProps) { return
{children}
; }