Skip to content

Commit 2c13154

Browse files
committed
feat: add Agno AgentOS chat example
1 parent f2c4c25 commit 2c13154

22 files changed

Lines changed: 848 additions & 18 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ openui/
175175
├── skills/
176176
│ └── openui/ # Claude Code skill for AI-assisted development
177177
├── examples/
178-
│ └── openui-chat/ # Full working example app (Next.js)
178+
│ ├── openui-chat/ # Full working example app (Next.js)
179+
│ └── agno-chat/ # AgentOS + OpenUI integration demo
179180
├── docs/ # Documentation site (openui.com)
180181
└── benchmarks/ # Token efficiency benchmarks
181182
```

docs/content/docs/agent/reference/agentinterface-props.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The only required prop is `llm`. Everything else is optional and falls back to a
2323
| `agentName` | `string` | No | none |
2424
| `starters` | `ConversationStarterProps[]` | No | none |
2525
| `starterVariant` | `"short" \| "long"` | No ||
26+
| `getThreadMenuActions` | `GetThreadMenuActions` | No | none |
2627
| `path` | `string` | No | — (uncontrolled) |
2728
| `defaultPath` | `string` | No | thread view (`undefined`) |
2829
| `onNavigate` | `(next: string \| undefined) => void` | No | — (uncontrolled) |
@@ -122,6 +123,31 @@ Set `disableThemeProvider` to `true` when `AgentInterface` is mounted inside an
122123

123124
These feed the default `SidebarHeader` and `MobileHeader`. To go further, replace those slots. See [Sidebar](/docs/agent/customize/sidebar).
124125

126+
## Thread menu actions
127+
128+
Use `getThreadMenuActions` to add context-aware links or callbacks to each thread's overflow menu. The callback receives `{ id, title, isSelected, isRunning }`, where `isRunning` is true only for the selected thread while its response is streaming.
129+
130+
```tsx
131+
<AgentInterface
132+
llm={llm}
133+
getThreadMenuActions={({ id, isRunning }) =>
134+
isRunning
135+
? []
136+
: [
137+
{
138+
id: "inspect-session",
139+
label: "Inspect session",
140+
href: `/sessions/${encodeURIComponent(id)}`,
141+
target: "_blank",
142+
rel: "noopener noreferrer",
143+
},
144+
]
145+
}
146+
/>;
147+
```
148+
149+
Each action must have a stable `id` and `label`, plus either `href` for a real anchor or `onSelect` for an in-app callback. An optional `icon` renders before the label.
150+
125151
## Starters
126152

127153
Conversation starters are the suggested prompts shown on the welcome screen and in the composer.

docs/content/docs/api-reference/react-ui.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const llm: ChatLLM = {
8989
- `labels?: AgentInterfaceLabels`
9090
- `starters?: ConversationStarterProps[]`
9191
- `starterVariant?: ConversationStarterVariant`
92+
- `getThreadMenuActions?: GetThreadMenuActions` — adds link or callback actions to each thread menu
9293
- `scrollVariant?: ScrollVariant`
9394
- `scrollOnLoad?: boolean`
9495
- Theme wrapper props:

examples/agno-chat/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules
2+
.venv
3+
dist
4+
.env
5+
.env.local
6+
src/generated
7+
tmp

examples/agno-chat/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Agno × OpenUI
2+
3+
This example demonstrates the complementary boundary:
4+
5+
```text
6+
AgentOS: agents · teams · tools · memory · knowledge · sessions · auth · execution
7+
8+
AG-UI
9+
10+
OpenUI: component contract · streaming parser · renderer · interactions · chat UI
11+
```
12+
13+
The browser uses `@openuidev/agno` for both channels expected by
14+
`AgentInterface`:
15+
16+
- `createAgnoLLM()` streams an AgentOS AG-UI run.
17+
- `agnoStorage()` stores the sidebar and message history in AgentOS sessions.
18+
19+
## Run without a model key
20+
21+
The Vite development server includes a deterministic AgentOS-compatible
22+
harness. It exercises session CRUD, Agno's empty tool-parent envelope, a backend
23+
tool result, streamed OpenUI Lang, follow-ups, and a validated form.
24+
25+
```bash
26+
pnpm dev
27+
```
28+
29+
Open `http://127.0.0.1:4173` and try both starters.
30+
31+
## Run with AgentOS
32+
33+
Configure the required model credential outside the repository, then:
34+
35+
```bash
36+
python3 -m venv .venv
37+
source .venv/bin/activate
38+
python -m pip install -r requirements.txt
39+
pnpm generate:prompt
40+
python server.py
41+
```
42+
43+
In another terminal, point the Vite proxy at AgentOS:
44+
45+
```bash
46+
AGNO_API_URL=http://127.0.0.1:7777 pnpm dev
47+
```
48+
49+
The React application does not change.
50+
51+
The Python server is deliberately ordinary Agno code: it owns the model, tool,
52+
database, history, and AG-UI interface. The component library and all rendering
53+
remain in the OpenUI frontend.

examples/agno-chat/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="description" content="Agno AgentOS with OpenUI generative UI" />
7+
<title>Agno × OpenUI</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

examples/agno-chat/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@openuidev/example-agno-chat",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"generate:prompt": "pnpm --filter @openuidev/cli build && pnpm exec openui generate src/library.ts --prompt-options promptOptions --out src/generated/system-prompt.txt",
8+
"dev": "vite",
9+
"build": "vite build",
10+
"preview": "vite preview",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"dependencies": {
14+
"@openuidev/agno": "workspace:*",
15+
"@openuidev/react-headless": "workspace:*",
16+
"@openuidev/react-lang": "workspace:*",
17+
"@openuidev/react-ui": "workspace:*",
18+
"lucide-react": "^0.562.0",
19+
"react": "^19.0.0",
20+
"react-dom": "^19.0.0",
21+
"zod": "^4.0.0",
22+
"zustand": "catalog:"
23+
},
24+
"devDependencies": {
25+
"@openuidev/cli": "workspace:*",
26+
"@types/node": "catalog:",
27+
"@types/react": "catalog:",
28+
"@types/react-dom": "catalog:",
29+
"typescript": "catalog:",
30+
"vite": "^6.0.0"
31+
}
32+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
agno[agui,openai,os,sqlite]>=2.9.0

examples/agno-chat/server.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Serve an Agno Agent through AgentOS while OpenUI owns the browser UI."""
2+
3+
from os import getenv
4+
from pathlib import Path
5+
6+
from agno.agent import Agent
7+
from agno.db.sqlite import SqliteDb
8+
from agno.models.openai import OpenAIResponses
9+
from agno.os import AgentOS
10+
from agno.os.interfaces.agui import AGUI
11+
from agno.tools import tool
12+
13+
EXAMPLE_ROOT = Path(__file__).resolve().parent
14+
OPENUI_PROMPT_PATH = EXAMPLE_ROOT / "src" / "generated" / "system-prompt.txt"
15+
16+
if not OPENUI_PROMPT_PATH.is_file():
17+
raise RuntimeError(
18+
"OpenUI system prompt is missing. Run `pnpm generate:prompt` in examples/agno-chat."
19+
)
20+
21+
22+
@tool
23+
def get_quarterly_revenue() -> dict:
24+
"""Return quarterly revenue in thousands of US dollars."""
25+
return {
26+
"currency": "USD",
27+
"unit": "thousands",
28+
"quarters": [
29+
{"quarter": "Q1", "revenue": 120},
30+
{"quarter": "Q2", "revenue": 180},
31+
{"quarter": "Q3", "revenue": 150},
32+
{"quarter": "Q4", "revenue": 240},
33+
],
34+
}
35+
36+
37+
agent = Agent(
38+
id="openui-assistant",
39+
name="Agno × OpenUI Assistant",
40+
model=OpenAIResponses(id=getenv("OPENAI_MODEL", "gpt-5.5")),
41+
db=SqliteDb(id="agno-openui", db_file="tmp/agno_openui.db"),
42+
tools=[get_quarterly_revenue],
43+
instructions=[
44+
"Use get_quarterly_revenue for stored revenue questions.",
45+
"Return only valid OpenUI Lang without Markdown fences.",
46+
OPENUI_PROMPT_PATH.read_text(encoding="utf-8"),
47+
],
48+
add_history_to_context=True,
49+
num_history_runs=10,
50+
)
51+
52+
agent_os = AgentOS(
53+
id="agno-openui-os",
54+
description="AgentOS owns the agent runtime; OpenUI owns the user interface.",
55+
agents=[agent],
56+
interfaces=[AGUI(agent=agent)],
57+
)
58+
app = agent_os.get_app()
59+
60+
if __name__ == "__main__":
61+
agent_os.serve(app=app)

examples/agno-chat/src/App.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { agnoStorage, createAgnoLLM } from "@openuidev/agno";
2+
import { AgentInterface } from "@openuidev/react-ui";
3+
import { ExternalLinkIcon } from "lucide-react";
4+
import { library } from "./library";
5+
6+
declare const __AGNO_BACKEND_MODE__: "real" | "mock";
7+
8+
const isRealAgentOS = __AGNO_BACKEND_MODE__ === "real";
9+
const DEMO_USER_ID = isRealAgentOS ? "openui-live-demo" : "openui-demo-user";
10+
const AGENT_ID = "openui-assistant";
11+
const AGENT_OS_URL = "https://os.agno.com";
12+
13+
const llm = createAgnoLLM({
14+
url: "/agui",
15+
forwardedProps: { user_id: DEMO_USER_ID },
16+
});
17+
18+
const storage = agnoStorage({
19+
baseUrl: "",
20+
entityType: "agent",
21+
entityId: AGENT_ID,
22+
userId: DEMO_USER_ID,
23+
});
24+
25+
const starters = [
26+
{
27+
displayText: "Use an Agno tool",
28+
prompt: "Use the stored quarterly revenue and show it as a chart with two useful follow-ups.",
29+
},
30+
{
31+
displayText: "Collect structured input",
32+
prompt: "Create a validated project estimate form with project name, team size, and notes.",
33+
},
34+
];
35+
36+
const agentOSSessionUrl = (sessionId: string) => {
37+
const url = new URL(`/sessions/${encodeURIComponent(sessionId)}`, AGENT_OS_URL);
38+
url.searchParams.set("sort_by", "updated_at_desc");
39+
url.searchParams.set("type", "all");
40+
url.searchParams.set("page", "1");
41+
url.searchParams.set("limit", "25");
42+
return url.toString();
43+
};
44+
45+
export default function App() {
46+
return (
47+
<AgentInterface
48+
llm={llm}
49+
storage={storage}
50+
componentLibrary={library}
51+
agentName="Agno × OpenUI"
52+
getThreadMenuActions={({ id, isRunning }) =>
53+
!isRealAgentOS || isRunning
54+
? []
55+
: [
56+
{
57+
id: "open-in-agentos",
58+
label: "Open in AgentOS",
59+
icon: <ExternalLinkIcon size="1em" />,
60+
href: agentOSSessionUrl(id),
61+
target: "_blank",
62+
rel: "noopener noreferrer",
63+
},
64+
]
65+
}
66+
starterVariant="short"
67+
starters={starters}
68+
>
69+
<AgentInterface.Welcome
70+
title="OpenUI handles the UI. AgentOS handles everything else."
71+
description={
72+
isRealAgentOS
73+
? "Connected to a live AgentOS and model. Try a backend Agno tool or ask for an interactive interface."
74+
: "Try a backend Agno tool or an interactive form. The local harness is deterministic and needs no model key."
75+
}
76+
/>
77+
</AgentInterface>
78+
);
79+
}

0 commit comments

Comments
 (0)