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
4 changes: 4 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
VITE_AGENTSTACK_BASE_URL="http://localhost:8333"

VITE_AGENTSTACK_PROVIDER_ID="983ab5fd-d7c9-0be1-77d9-442a68c5949e"

24 changes: 24 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
10 changes: 10 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import baseConfig from '@i-am-bee/lint-config/eslint';
import { defineConfig } from 'eslint/config';
import reactHooks from 'eslint-plugin-react-hooks';

export default defineConfig([...baseConfig, reactHooks.configs.flat['recommended-latest']]);
20 changes: 20 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Copyright 2025 © BeeAI a Series of LF Projects, LLC
SPDX-License-Identifier: Apache-2.0
-->

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>Agent Stack Chat Example</title>
</head>

<body>
<div id="root"></div>

<script type="module" src="/src/main.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@i-am-bee/chat-ui-example",
"author": "IBM Corp.",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@a2a-js/sdk": "^0.3.10",
"agentstack-sdk": "^0.6.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@i-am-bee/lint-config": "workspace:*",
"@types/node": "^22.19.10",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1",
"prettier": "^3.8.1",
"stylelint": "^16.26.1",
"typescript": "5.9.3",
"vite": "^7.3.1"
},
"packageManager": "pnpm@9.9.0"
}
6 changes: 6 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/prettier.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

export { default } from '@i-am-bee/lint-config/prettier';
117 changes: 117 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { type SubmitEvent, useState } from 'react';

import { useAgent } from './client';
import { BASE_URL, PROVIDER_ID } from './constants';
import type { ChatMessage } from './types';
import { createMessage } from './utils';

export function App() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState('');
const [isSending, setIsSending] = useState(false);

const { session, isInitializing, error, sendMessage } = useAgent();

const isError = Boolean(error);
const isSubmitDisabled = isInitializing || isSending || isError || !input.trim();

const handleSubmit = async (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();

if (isSubmitDisabled) {
return;
}

const text = input.trim();

setMessages((prevMessages) => [...prevMessages, createMessage({ role: 'user', text })]);
setInput('');
setIsSending(true);

try {
const response = await sendMessage({ text });

setMessages((prevMessages) => [
...prevMessages,
createMessage({ role: 'agent', text: response.text || 'No response from agent.' }),
]);
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to send message';

setMessages((prevMessages) => [...prevMessages, createMessage({ role: 'agent', text: `Error: ${message}` })]);
} finally {
setIsSending(false);
}
};

return (
<main className="app">
<h1 className="heading">Agent Stack Chat Example</h1>

<div className="meta">
<p>
<strong>Base URL:</strong> {BASE_URL}
</p>

<p>
<strong>Provider ID:</strong> {PROVIDER_ID}
</p>

<p>
<strong>Context ID:</strong> {session?.contextId}
</p>
</div>

<div className="controls">
<button className="button" type="button" onClick={() => window.location.reload()}>
New session
</button>
</div>

{messages.length > 0 && (
<section className="messages">
{messages.map((message) => (
<article key={message.id} className={`message ${message.role}`}>
<header>{message.role}</header>

<p>{message.text}</p>
</article>
))}
</section>
)}

<div className="meta">
<p>
<strong>Status:</strong>{' '}
{isError ? 'Error' : isInitializing ? 'Connecting to agent…' : isSending ? 'Agent is thinking…' : 'Ready'}
</p>

{isError && (
<p className="error">
<strong>Error:</strong> {error}
</p>
)}
</div>

<form className="form" onSubmit={handleSubmit}>
<input
className="input"
type="text"
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder="Type a message…"
autoFocus
/>

<button className="button" type="submit" disabled={isSubmitDisabled}>
Send
</button>
</form>
</main>
);
}
31 changes: 31 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add documentation page where you link this example as well as some description and overall page descirbing what you had to do etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jenna-winkler what's your take on this.

I was hoping we would squeze whole workign example in the docs itself, but that would make the page extremly long.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes let’s do a docs page - a guide on approach to building and link to example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new Custom UI Architecture Guide page to docs. I'd appreciate any feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @PetrBulanek ! @sandijean90 can you please review ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great @PetrBulanek

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m merging this so we can include it in the RC release.

@sandijean90 If you have any comments or concerns, please open a follow-up issue and I can address them there.

Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PetrBulanek @jenna-winkler thanks! I will take a look today/early next week at the whole Client SDk docs section as I need to familiarize myself before making comments/edits.

* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { buildApiClient, unwrapResult } from 'agentstack-sdk';

import { BASE_URL, PROVIDER_ID } from './constants';

export const api = buildApiClient({ baseUrl: BASE_URL });

export async function createContext() {
const result = await api.createContext({ provider_id: PROVIDER_ID });

return unwrapResult(result);
}

export async function createContextToken(contextId: string) {
const result = await api.createContextToken({
context_id: contextId,
grant_global_permissions: {
a2a_proxy: [PROVIDER_ID],
llm: ['*'],
},
grant_context_permissions: {
context_data: ['*'],
},
});

return unwrapResult(result);
}
142 changes: 142 additions & 0 deletions apps/agentstack-sdk-ts/examples/chat-ui/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
ClientFactory,
ClientFactoryOptions,
DefaultAgentCardResolver,
JsonRpcTransportFactory,
} from '@a2a-js/sdk/client';
import { type ContextToken, createAuthenticatedFetch, getAgentCardPath } from 'agentstack-sdk';
import { useEffect, useState } from 'react';

import { createContext, createContextToken } from './api';
import { BASE_URL, PROVIDER_ID } from './constants';
import type { Session } from './types';
import { extractTextFromMessage, resolveAgentMetadata } from './utils';

async function ensureSession() {
if (!BASE_URL || !PROVIDER_ID) {
throw new Error(`Missing required environment variables.`);
}

const context = await createContext();
const contextToken = await createContextToken(context.id);
const client = await createClient(contextToken);
const metadata = await resolveAgentMetadata({ client, contextToken });

return {
client,
contextId: context.id,
metadata,
};
}

async function createClient(contextToken: ContextToken) {
const fetchImpl = createAuthenticatedFetch(contextToken.token);

const factory = new ClientFactory(
ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
transports: [new JsonRpcTransportFactory({ fetchImpl })],
cardResolver: new DefaultAgentCardResolver({ fetchImpl }),
}),
);

const agentCardPath = getAgentCardPath(PROVIDER_ID);
const client = await factory.createFromUrl(BASE_URL, agentCardPath);

return client;
}
Comment on lines +37 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would make sense to create an abstraction for this in our sdk?


export function useAgent() {
const [session, setSession] = useState<Session>();
const [isInitializing, setIsInitializing] = useState(false);
const [error, setError] = useState('');

useEffect(() => {
if (session) {
return;
}

let cancelled = false;

(async () => {
try {
setIsInitializing(true);

const session = await ensureSession();

if (cancelled) {
return;
}

setSession(session);
} catch (error) {
if (cancelled) {
return;
}

const message = error instanceof Error ? error.message : 'Failed to connect to agent.';

setError(message);
} finally {
if (!cancelled) {
setIsInitializing(false);
}
}
})();

return () => {
cancelled = true;
};
}, [session]);

const sendMessage = async ({ text }: { text: string }) => {
if (!session) {
throw new Error('Agent is not ready yet.');
}

const { client, contextId, metadata } = session;

const runStream = async () => {
const stream = client.sendMessageStream({
message: {
kind: 'message',
role: 'user',
messageId: crypto.randomUUID(),
contextId,
parts: [{ kind: 'text', text }],
metadata,
},
});

let agentText = '';

for await (const event of stream) {
if (event.kind === 'status-update' || event.kind === 'message') {
const message = event.kind === 'message' ? event : event.status.message;
const text = extractTextFromMessage(message);

if (text) {
agentText += text;
}
}
}

return {
text: agentText,
};
};

return await runStream();
};

return {
session,
isInitializing,
error,
sendMessage,
};
}
Loading
Loading