@genui-a3/create 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +98 -0
- package/package.json +45 -0
- package/template/README.md +16 -0
- package/template/_gitignore +35 -0
- package/template/app/(pages)/agui/AguiChat.tsx +109 -0
- package/template/app/(pages)/agui/page.tsx +12 -0
- package/template/app/(pages)/chat/page.tsx +12 -0
- package/template/app/(pages)/stream/page.tsx +12 -0
- package/template/app/ThemeProvider.tsx +17 -0
- package/template/app/agents/age.ts +24 -0
- package/template/app/agents/greeting.ts +37 -0
- package/template/app/api/agui/route.ts +69 -0
- package/template/app/api/chat/route.ts +65 -0
- package/template/app/api/stream/route.ts +64 -0
- package/template/app/apple-icon-dark.png +0 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/components/atoms/AppLogo.tsx +44 -0
- package/template/app/components/atoms/ChatContainer.tsx +13 -0
- package/template/app/components/atoms/ChatHeader.tsx +14 -0
- package/template/app/components/atoms/MessageBubble.tsx +21 -0
- package/template/app/components/atoms/index.ts +4 -0
- package/template/app/components/molecules/ChatInput.tsx +66 -0
- package/template/app/components/molecules/ChatMessage.tsx +41 -0
- package/template/app/components/molecules/index.ts +2 -0
- package/template/app/components/organisms/Chat.tsx +77 -0
- package/template/app/components/organisms/ChatMessageList.tsx +37 -0
- package/template/app/components/organisms/PageLayout.tsx +43 -0
- package/template/app/components/organisms/StreamChat.tsx +149 -0
- package/template/app/components/organisms/index.ts +4 -0
- package/template/app/constants/chat.ts +4 -0
- package/template/app/favicon-dark.ico +0 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/icon.svg +13 -0
- package/template/app/layout.tsx +32 -0
- package/template/app/page.tsx +171 -0
- package/template/app/styled.d.ts +6 -0
- package/template/app/theme.ts +22 -0
- package/template/app/types/index.ts +8 -0
- package/template/next.config.mjs +6 -0
- package/template/package.json +29 -0
- package/template/public/android-chrome-192x192.png +0 -0
- package/template/public/android-chrome-512x512.png +0 -0
- package/template/public/site.webmanifest +11 -0
- package/template/tsconfig.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import fsExtra from "fs-extra";
|
|
9
|
+
import prompts from "prompts";
|
|
10
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
var __dirname = path.dirname(__filename);
|
|
12
|
+
var banner = `
|
|
13
|
+
${chalk.cyan("\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ")}
|
|
14
|
+
${chalk.cyan("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2557")}
|
|
15
|
+
${chalk.cyan("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
|
|
16
|
+
${chalk.cyan("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u255A\u2550\u2550\u2550\u2588\u2588\u2557")}
|
|
17
|
+
${chalk.cyan("\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
|
|
18
|
+
${chalk.cyan("\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D ")}
|
|
19
|
+
|
|
20
|
+
${chalk.white.bold("GenUI A3")} ${chalk.dim("\u2014 Agentic App Scaffold")}
|
|
21
|
+
`;
|
|
22
|
+
async function main() {
|
|
23
|
+
console.log(banner);
|
|
24
|
+
let projectName = process.argv[2];
|
|
25
|
+
if (!projectName) {
|
|
26
|
+
const response = await prompts(
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
name: "projectName",
|
|
30
|
+
message: "What is your project named?",
|
|
31
|
+
initial: "my-a3-quickstart"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
onCancel: () => {
|
|
35
|
+
console.log(chalk.red("\nSetup cancelled."));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
projectName = response.projectName;
|
|
41
|
+
}
|
|
42
|
+
if (!projectName) {
|
|
43
|
+
console.error(chalk.red("Project name is required."));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
47
|
+
if (fsExtra.existsSync(targetDir)) {
|
|
48
|
+
const contents = fsExtra.readdirSync(targetDir);
|
|
49
|
+
if (contents.length > 0) {
|
|
50
|
+
console.error(chalk.red(`
|
|
51
|
+
Directory "${projectName}" already exists and is not empty.`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const templateDir = path.resolve(__dirname, "..", "template");
|
|
56
|
+
if (!fsExtra.existsSync(templateDir)) {
|
|
57
|
+
console.error(chalk.red("\nTemplate directory not found. The package may not have been built correctly."));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk.dim(`
|
|
61
|
+
Creating a new A3 app in ${chalk.bold(targetDir)}...
|
|
62
|
+
`));
|
|
63
|
+
fsExtra.copySync(templateDir, targetDir);
|
|
64
|
+
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
65
|
+
if (fsExtra.existsSync(pkgJsonPath)) {
|
|
66
|
+
const pkg = fsExtra.readJsonSync(pkgJsonPath);
|
|
67
|
+
pkg.name = projectName;
|
|
68
|
+
delete pkg.private;
|
|
69
|
+
fsExtra.writeJsonSync(pkgJsonPath, pkg, { spaces: 2 });
|
|
70
|
+
}
|
|
71
|
+
const gitignoreSrc = path.join(targetDir, "_gitignore");
|
|
72
|
+
const gitignoreDest = path.join(targetDir, ".gitignore");
|
|
73
|
+
if (fsExtra.existsSync(gitignoreSrc)) {
|
|
74
|
+
fsExtra.renameSync(gitignoreSrc, gitignoreDest);
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.cyan("Installing dependencies...\n"));
|
|
77
|
+
try {
|
|
78
|
+
execSync("npm install", {
|
|
79
|
+
cwd: targetDir,
|
|
80
|
+
stdio: "inherit"
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
console.error(chalk.red("\nFailed to install dependencies. You can try manually:"));
|
|
84
|
+
console.log(` cd ${projectName}`);
|
|
85
|
+
console.log(" npm install\n");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
console.log(`
|
|
89
|
+
${chalk.green.bold("Success!")} Created ${chalk.bold(projectName)} at ${chalk.dim(targetDir)}
|
|
90
|
+
`);
|
|
91
|
+
console.log("Get started:\n");
|
|
92
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
93
|
+
console.log(chalk.cyan(" npm run dev\n"));
|
|
94
|
+
}
|
|
95
|
+
main().catch((err) => {
|
|
96
|
+
console.error(chalk.red("Unexpected error:"), err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@genui-a3/create",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI scaffolding tool for GenUI A3 agentic apps",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"genui",
|
|
7
|
+
"a3",
|
|
8
|
+
"agentic",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"author": "",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"bin": {
|
|
17
|
+
"create-genui-a3": "dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"template"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "npm run prepare-template && tsup",
|
|
25
|
+
"postbuild": "chmod +x dist/index.js",
|
|
26
|
+
"clean": "rm -rf dist template",
|
|
27
|
+
"prepare-template": "node scripts/prepare-template.mjs",
|
|
28
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"chalk": "5.4.1",
|
|
35
|
+
"fs-extra": "11.3.0",
|
|
36
|
+
"prompts": "2.4.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/fs-extra": "11.0.4",
|
|
40
|
+
"@types/node": "20.19.0",
|
|
41
|
+
"@types/prompts": "2.4.9",
|
|
42
|
+
"tsup": "8.5.1",
|
|
43
|
+
"typescript": "5.9.3"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# A3 App
|
|
2
|
+
|
|
3
|
+
Built with [GenUI A3](https://www.npmjs.com/package/@genui-a3/core).
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
|
12
|
+
|
|
13
|
+
## Learn More
|
|
14
|
+
|
|
15
|
+
- [A3 Core Documentation](https://www.npmjs.com/package/@genui-a3/core)
|
|
16
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
|
|
8
|
+
# testing
|
|
9
|
+
/coverage
|
|
10
|
+
|
|
11
|
+
# next.js
|
|
12
|
+
/.next/
|
|
13
|
+
/out/
|
|
14
|
+
|
|
15
|
+
# production
|
|
16
|
+
/build
|
|
17
|
+
|
|
18
|
+
# misc
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.pem
|
|
21
|
+
|
|
22
|
+
# debug
|
|
23
|
+
npm-debug.log*
|
|
24
|
+
yarn-debug.log*
|
|
25
|
+
yarn-error.log*
|
|
26
|
+
|
|
27
|
+
# local env files
|
|
28
|
+
.env*.local
|
|
29
|
+
|
|
30
|
+
# vercel
|
|
31
|
+
.vercel
|
|
32
|
+
|
|
33
|
+
# typescript
|
|
34
|
+
*.tsbuildinfo
|
|
35
|
+
next-env.d.ts
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react'
|
|
4
|
+
import { Typography } from '@mui/material'
|
|
5
|
+
import { ChatMessageList } from '@organisms/ChatMessageList'
|
|
6
|
+
import { ChatContainer, ChatHeader } from '@atoms'
|
|
7
|
+
import { ChatInput } from '@molecules'
|
|
8
|
+
import type { ChatMessage as ChatMessageType } from 'types'
|
|
9
|
+
import { HttpAgent, EventType } from '@ag-ui/client'
|
|
10
|
+
|
|
11
|
+
const agent = new HttpAgent({
|
|
12
|
+
url: '/api/agui',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export function AguiChat() {
|
|
16
|
+
const [messages, setMessages] = useState<ChatMessageType[]>([])
|
|
17
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
18
|
+
const assistantIdRef = useRef<string>('')
|
|
19
|
+
|
|
20
|
+
const handleSubmit = useCallback(async (text: string) => {
|
|
21
|
+
const userMsg: ChatMessageType = {
|
|
22
|
+
id: crypto.randomUUID(),
|
|
23
|
+
body: text,
|
|
24
|
+
source: 'user',
|
|
25
|
+
}
|
|
26
|
+
setMessages((prev) => [...prev, userMsg])
|
|
27
|
+
setIsLoading(true)
|
|
28
|
+
|
|
29
|
+
let assistantId = crypto.randomUUID()
|
|
30
|
+
assistantIdRef.current = assistantId
|
|
31
|
+
|
|
32
|
+
const streamingMsg: ChatMessageType = {
|
|
33
|
+
id: assistantId,
|
|
34
|
+
body: '',
|
|
35
|
+
source: 'assistant',
|
|
36
|
+
isStreaming: true,
|
|
37
|
+
}
|
|
38
|
+
setMessages((prev) => [...prev, streamingMsg])
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const runId = crypto.randomUUID()
|
|
42
|
+
|
|
43
|
+
// Add the user message to the agent's internal state BEFORE running.
|
|
44
|
+
agent.addMessage({ id: crypto.randomUUID(), role: 'user', content: text })
|
|
45
|
+
|
|
46
|
+
await agent.runAgent(
|
|
47
|
+
{
|
|
48
|
+
runId,
|
|
49
|
+
tools: [],
|
|
50
|
+
context: [],
|
|
51
|
+
forwardedProps: {},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
onEvent({ event }) {
|
|
55
|
+
if (event.type === EventType.TEXT_MESSAGE_CONTENT && 'delta' in event) {
|
|
56
|
+
const delta = (event as unknown as { delta: string }).delta
|
|
57
|
+
setMessages((prev) => prev.map((m) => (m.id === assistantId ? { ...m, body: m.body + delta } : m)))
|
|
58
|
+
} else if (event.type === EventType.CUSTOM && 'name' in event) {
|
|
59
|
+
const customEvent = event as unknown as { name: string }
|
|
60
|
+
if (customEvent.name === 'AgentTransition') {
|
|
61
|
+
const prevAssistantId = assistantId
|
|
62
|
+
assistantId = crypto.randomUUID()
|
|
63
|
+
assistantIdRef.current = assistantId
|
|
64
|
+
setMessages((prev) => {
|
|
65
|
+
const updated = prev.map((m) => (m.id === prevAssistantId ? { ...m, isStreaming: false } : m))
|
|
66
|
+
return [...updated, { id: assistantId, body: '', source: 'assistant', isStreaming: true }]
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
} else if (event.type === EventType.RUN_FINISHED) {
|
|
70
|
+
setMessages((prev) => prev.map((m) => (m.id === assistantId ? { ...m, isStreaming: false } : m)))
|
|
71
|
+
} else if (event.type === EventType.RUN_ERROR) {
|
|
72
|
+
setMessages((prev) =>
|
|
73
|
+
prev.map((m) =>
|
|
74
|
+
m.id === assistantId
|
|
75
|
+
? { ...m, body: m.body || 'Sorry, something went wrong.', isStreaming: false }
|
|
76
|
+
: m,
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('AG-UI chat error:', error)
|
|
85
|
+
setMessages((prev) =>
|
|
86
|
+
prev.map((m) =>
|
|
87
|
+
m.id === assistantId
|
|
88
|
+
? { ...m, body: m.body || 'Sorry, something went wrong. Please try again.', isStreaming: false }
|
|
89
|
+
: m,
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
} finally {
|
|
93
|
+
setIsLoading(false)
|
|
94
|
+
setMessages((prev) => prev.map((m) => (m.id === assistantId ? { ...m, isStreaming: false } : m)))
|
|
95
|
+
}
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ChatContainer elevation={0}>
|
|
100
|
+
<ChatHeader>
|
|
101
|
+
<Typography variant="h6" component="h2">
|
|
102
|
+
Chat (AG-UI Protocol)
|
|
103
|
+
</Typography>
|
|
104
|
+
</ChatHeader>
|
|
105
|
+
<ChatMessageList messages={messages} />
|
|
106
|
+
<ChatInput onSubmit={handleSubmit} disabled={isLoading} />
|
|
107
|
+
</ChatContainer>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
|
|
4
|
+
import { ThemeProvider as StyledThemeProvider } from 'styled-components'
|
|
5
|
+
import CssBaseline from '@mui/material/CssBaseline'
|
|
6
|
+
import { theme } from './theme'
|
|
7
|
+
|
|
8
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<MuiThemeProvider theme={theme}>
|
|
11
|
+
<StyledThemeProvider theme={theme}>
|
|
12
|
+
<CssBaseline />
|
|
13
|
+
{children}
|
|
14
|
+
</StyledThemeProvider>
|
|
15
|
+
</MuiThemeProvider>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { Agent } from '@genui-a3/core'
|
|
3
|
+
import { State } from './greeting'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sample age agent that gets the user's age.
|
|
7
|
+
*/
|
|
8
|
+
const agePayload = z.object({
|
|
9
|
+
userAge: z.string().optional(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const ageAgent: Agent<State> = {
|
|
13
|
+
id: 'age',
|
|
14
|
+
prompt: `
|
|
15
|
+
You are a friendly agent. Your goal is to learn the user's age.
|
|
16
|
+
If you don't know their age yet, ask for it politely.
|
|
17
|
+
Once you have their age, confirm it and set goalAchieved to true.
|
|
18
|
+
|
|
19
|
+
If the user says they want to change their name or use a different name,
|
|
20
|
+
say "Of course, I can help you with that" and transition to the greeting agent.
|
|
21
|
+
`,
|
|
22
|
+
outputSchema: agePayload,
|
|
23
|
+
transition: ['greeting'],
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { Agent, BaseState } from '@genui-a3/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Consumer defines their GLOBAL state extending BaseState.
|
|
6
|
+
* This state is shared across ALL agents in the session.
|
|
7
|
+
*/
|
|
8
|
+
export interface State extends BaseState {
|
|
9
|
+
userName?: string
|
|
10
|
+
userAge?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sample greeting agent that demonstrates the AgentRegistry pattern.
|
|
15
|
+
*/
|
|
16
|
+
const greetingPayload = z.object({
|
|
17
|
+
userName: z.string().optional(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const greetingAgent: Agent<State> = {
|
|
21
|
+
id: 'greeting',
|
|
22
|
+
prompt: `
|
|
23
|
+
You are a friendly greeting agent. Your goal is to greet the user and learn their name.
|
|
24
|
+
You also handle name changes — if the user wants to update their name, collect the new one.
|
|
25
|
+
If you don't know their name yet, ask for it politely.
|
|
26
|
+
Once you have their name, greet them by name and set goalAchieved to true.
|
|
27
|
+
|
|
28
|
+
Do not ask "How can I help you today?".
|
|
29
|
+
`,
|
|
30
|
+
outputSchema: greetingPayload,
|
|
31
|
+
transition: (state, agentGoalAchieved) => {
|
|
32
|
+
if (agentGoalAchieved) {
|
|
33
|
+
return 'age'
|
|
34
|
+
}
|
|
35
|
+
return 'greeting'
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { EventType, type RunAgentInput } from '@ag-ui/client'
|
|
3
|
+
import { EventEncoder } from '@ag-ui/encoder'
|
|
4
|
+
import { AgentRegistry, ChatSession, MemorySessionStore, AGUIAgent } from '@genui-a3/core'
|
|
5
|
+
import { greetingAgent, State } from '../../agents/greeting'
|
|
6
|
+
import { ageAgent } from '../../agents/age'
|
|
7
|
+
|
|
8
|
+
const registry = AgentRegistry.getInstance<State>()
|
|
9
|
+
if (!registry.has('greeting')) {
|
|
10
|
+
registry.register(greetingAgent)
|
|
11
|
+
}
|
|
12
|
+
if (!registry.has('age')) {
|
|
13
|
+
registry.register(ageAgent)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const store = new MemorySessionStore<State>()
|
|
17
|
+
|
|
18
|
+
const a3Agent = new AGUIAgent({
|
|
19
|
+
agentId: 'a3-demo',
|
|
20
|
+
createSession: (input: RunAgentInput) =>
|
|
21
|
+
new ChatSession<State>({
|
|
22
|
+
sessionId: input.threadId,
|
|
23
|
+
store,
|
|
24
|
+
initialAgentId: 'greeting',
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export async function POST(request: NextRequest) {
|
|
29
|
+
const body = (await request.json()) as RunAgentInput
|
|
30
|
+
|
|
31
|
+
const encoder = new EventEncoder()
|
|
32
|
+
const events$ = a3Agent.run(body)
|
|
33
|
+
|
|
34
|
+
const stream = new ReadableStream({
|
|
35
|
+
start(controller) {
|
|
36
|
+
const textEncoder = new TextEncoder()
|
|
37
|
+
const subscription = events$.subscribe({
|
|
38
|
+
next(event) {
|
|
39
|
+
controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)))
|
|
40
|
+
},
|
|
41
|
+
error(err) {
|
|
42
|
+
const errorEvent = {
|
|
43
|
+
type: EventType.RUN_ERROR,
|
|
44
|
+
message: String(err),
|
|
45
|
+
}
|
|
46
|
+
controller.enqueue(textEncoder.encode(encoder.encodeSSE(errorEvent)))
|
|
47
|
+
controller.close()
|
|
48
|
+
},
|
|
49
|
+
complete() {
|
|
50
|
+
controller.close()
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Clean up on cancel
|
|
55
|
+
request.signal.addEventListener('abort', () => {
|
|
56
|
+
subscription.unsubscribe()
|
|
57
|
+
controller.close()
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return new Response(stream, {
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': encoder.getContentType(),
|
|
65
|
+
'Cache-Control': 'no-cache',
|
|
66
|
+
Connection: 'keep-alive',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous (blocking / unary) chat endpoint.
|
|
3
|
+
* This is the non-streaming version of the /api/stream endpoint.
|
|
4
|
+
* It waits for the full agent response before returning a complete JSON payload.
|
|
5
|
+
*/
|
|
6
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
7
|
+
import { AgentRegistry, ChatSession, MemorySessionStore } from '@genui-a3/core'
|
|
8
|
+
import { greetingAgent, State } from '../../agents/greeting'
|
|
9
|
+
import { ageAgent } from '../../agents/age'
|
|
10
|
+
|
|
11
|
+
// Register the agent on module load
|
|
12
|
+
const registry = AgentRegistry.getInstance<State>()
|
|
13
|
+
if (!registry.has('greeting')) {
|
|
14
|
+
registry.register(greetingAgent)
|
|
15
|
+
}
|
|
16
|
+
if (!registry.has('age')) {
|
|
17
|
+
registry.register(ageAgent)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Shared store instance (in production, use Redis/DynamoDB)
|
|
21
|
+
const store = new MemorySessionStore<State>()
|
|
22
|
+
|
|
23
|
+
export async function POST(request: NextRequest) {
|
|
24
|
+
try {
|
|
25
|
+
const body = (await request.json()) as { message?: string; sessionId?: string }
|
|
26
|
+
const { message, sessionId = 'demo-session' } = body
|
|
27
|
+
|
|
28
|
+
if (!message) {
|
|
29
|
+
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create session and send message
|
|
33
|
+
const session = new ChatSession<State>({
|
|
34
|
+
sessionId,
|
|
35
|
+
store,
|
|
36
|
+
initialAgentId: 'greeting',
|
|
37
|
+
initialState: { userName: undefined },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const result = await session.send(message)
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
response: result.responseMessage,
|
|
44
|
+
activeAgentId: result.activeAgentId,
|
|
45
|
+
nextAgentId: result.nextAgentId,
|
|
46
|
+
state: result.state,
|
|
47
|
+
goalAchieved: result.goalAchieved,
|
|
48
|
+
})
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Chat error:', error)
|
|
51
|
+
return NextResponse.json({ error: 'Internal server error', details: String(error) }, { status: 500 })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// GET endpoint to list available agents
|
|
56
|
+
export function GET() {
|
|
57
|
+
const agents = AgentRegistry.getInstance().getAll()
|
|
58
|
+
return NextResponse.json({
|
|
59
|
+
agents: agents.map((agent) => ({
|
|
60
|
+
id: agent.id,
|
|
61
|
+
name: agent.name,
|
|
62
|
+
description: agent.description,
|
|
63
|
+
})),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { AgentRegistry, ChatSession, MemorySessionStore } from '@genui-a3/core'
|
|
3
|
+
import { greetingAgent, State } from '../../agents/greeting'
|
|
4
|
+
import { ageAgent } from '../../agents/age'
|
|
5
|
+
|
|
6
|
+
// Guard: agent may already be registered by the non-streaming route
|
|
7
|
+
const registry = AgentRegistry.getInstance<State>()
|
|
8
|
+
if (!registry.has('greeting')) {
|
|
9
|
+
registry.register(greetingAgent)
|
|
10
|
+
}
|
|
11
|
+
if (!registry.has('age')) {
|
|
12
|
+
registry.register(ageAgent)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const store = new MemorySessionStore<State>()
|
|
16
|
+
|
|
17
|
+
export async function POST(request: NextRequest) {
|
|
18
|
+
const body = (await request.json()) as { message?: string; sessionId?: string }
|
|
19
|
+
const { message, sessionId = 'demo-stream-session' } = body
|
|
20
|
+
|
|
21
|
+
if (!message) {
|
|
22
|
+
return new Response(JSON.stringify({ error: 'Message is required' }), {
|
|
23
|
+
status: 400,
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const session = new ChatSession<State>({
|
|
29
|
+
sessionId,
|
|
30
|
+
store,
|
|
31
|
+
initialAgentId: 'greeting',
|
|
32
|
+
initialState: { userName: undefined },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const encoder = new TextEncoder()
|
|
36
|
+
|
|
37
|
+
const stream = new ReadableStream({
|
|
38
|
+
async start(controller) {
|
|
39
|
+
try {
|
|
40
|
+
for await (const event of session.sendStream(message)) {
|
|
41
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
|
|
42
|
+
}
|
|
43
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const errorEvent = {
|
|
46
|
+
type: 'error',
|
|
47
|
+
error: { message: String(error) },
|
|
48
|
+
agentId: 'unknown',
|
|
49
|
+
}
|
|
50
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`))
|
|
51
|
+
} finally {
|
|
52
|
+
controller.close()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return new Response(stream, {
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'text/event-stream',
|
|
60
|
+
'Cache-Control': 'no-cache',
|
|
61
|
+
Connection: 'keep-alive',
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const MARK_PATH =
|
|
2
|
+
'M 560.50 870.11 C544.60,872.39 493.52,873.54 477.50,871.98 C429.64,867.32 381.89,853.57 340.50,832.53 C316.53,820.34 296.91,807.54 274.00,789.15 C261.51,779.12 236.58,754.18 225.88,741.00 C180.72,685.39 153.06,618.33 146.08,547.58 C144.70,533.59 144.69,497.56 146.06,483.42 C151.82,424.12 172.58,365.85 205.33,317.00 C257.37,239.38 349.03,180.93 442.00,166.07 C464.04,162.55 478.67,161.69 505.88,162.29 C555.48,163.38 594.16,171.21 639.00,189.25 C659.38,197.45 694.34,215.75 702.79,222.64 L 705.07 224.50 L 699.18 234.00 C695.94,239.23 690.48,248.11 687.05,253.75 C683.61,259.39 680.44,264.00 679.99,264.00 C679.54,264.00 674.52,261.04 668.84,257.42 C655.11,248.69 625.53,234.22 610.00,228.66 C583.75,219.25 553.35,212.43 527.00,210.05 C512.11,208.70 478.67,208.71 464.00,210.07 C448.87,211.47 421.38,216.98 407.00,221.49 C359.48,236.39 317.14,262.02 281.44,297.50 C258.31,320.48 241.26,343.77 226.14,373.00 C202.42,418.87 191.69,463.62 191.74,516.50 C191.78,555.23 197.10,585.98 210.10,622.48 C215.21,636.83 229.38,665.53 238.40,679.79 C258.58,711.72 284.51,739.87 314.60,762.50 C358.47,795.49 416.85,818.66 475.00,826.15 C490.90,828.19 536.15,827.93 552.00,825.69 C589.08,820.47 619.66,811.35 651.80,795.94 C758.47,744.79 825.34,642.74 828.69,526.00 C830.16,474.91 818.90,425.90 794.39,376.67 C790.88,369.61 788.00,363.68 788.00,363.49 C788.00,362.88 830.91,349.00 832.81,349.00 C834.20,349.00 836.30,352.42 841.26,362.75 C849.43,379.78 850.04,381.23 856.16,398.49 C887.34,486.35 882.15,583.99 841.89,667.00 C819.83,712.49 793.79,746.96 757.00,779.42 C700.79,829.00 634.78,859.47 560.50,870.11 ZM 335.80 648.00 L 331.50 660.50 L 297.25 660.76 C278.41,660.91 263.00,660.77 263.00,660.45 C263.00,659.86 278.07,617.38 291.17,581.00 C295.04,570.28 308.24,533.38 320.52,499.00 C332.80,464.62 347.87,422.55 354.01,405.50 C360.15,388.45 366.62,370.45 368.39,365.50 L 371.61 356.50 L 409.10 356.24 L 446.59 355.98 L 457.22 385.74 C463.07,402.11 475.32,436.42 484.45,462.00 C493.58,487.58 502.36,512.10 503.97,516.50 C505.58,520.90 515.27,547.90 525.51,576.50 C535.75,605.10 546.52,635.12 549.43,643.22 C552.35,651.31 554.49,658.17 554.20,658.47 C553.91,658.76 537.97,659.00 518.78,659.00 L 483.90 659.00 L 482.58 655.75 C481.85,653.96 477.26,640.69 472.38,626.26 L 463.50 600.02 L 352.45 600.00 L 346.28 617.75 C342.88,627.51 338.17,641.12 335.80,648.00 ZM 687.50 661.35 C660.93,665.46 637.94,663.57 615.06,655.41 C601.27,650.49 591.03,644.01 580.37,633.46 C572.36,625.53 570.61,623.14 566.13,613.96 C560.20,601.81 557.14,589.54 556.76,576.32 L 556.50 567.50 L 616.57 566.97 L 617.62 574.74 C620.91,598.97 640.94,614.52 666.50,612.68 C689.18,611.04 704.64,597.72 707.93,576.97 C709.92,564.44 706.02,552.13 697.07,542.67 C687.39,532.43 676.26,529.04 652.23,529.02 L 636.96 529.00 L 637.23 504.75 L 637.50 480.50 L 654.00 479.93 C674.62,479.21 680.48,477.31 690.26,468.16 C697.55,461.34 700.26,455.37 700.77,445.00 C701.35,433.25 698.88,426.59 691.06,418.89 C682.94,410.89 676.70,408.61 663.00,408.61 C649.61,408.61 643.48,410.81 635.00,418.66 C628.14,425.01 625.48,428.98 622.42,437.42 L 620.22 443.50 L 591.93 443.77 C576.36,443.91 563.30,443.69 562.90,443.27 C561.92,442.23 564.65,426.97 567.16,419.51 C578.07,387.00 606.35,365.08 645.92,358.47 C659.23,356.24 682.31,356.90 695.19,359.88 C714.30,364.30 729.13,372.16 741.60,384.49 C755.80,398.54 761.90,412.81 762.75,434.04 C763.17,444.62 762.92,447.89 761.13,455.04 C756.76,472.50 747.16,486.30 733.31,495.04 C729.72,497.32 727.16,499.52 727.64,499.94 C728.11,500.37 731.08,501.76 734.24,503.03 C747.92,508.57 761.40,522.70 767.11,537.47 C771.91,549.87 773.33,559.36 772.69,574.50 C772.04,589.80 770.30,597.27 764.55,609.50 C751.70,636.85 723.57,655.78 687.50,661.35 ZM 370.99 545.75 C371.00,546.73 379.19,547.00 408.61,547.00 C438.28,547.00 446.12,546.74 445.75,545.75 C445.49,545.06 437.02,519.64 426.93,489.25 C416.83,458.86 408.31,434.00 407.99,434.00 C407.33,434.00 370.98,543.79 370.99,545.75 Z'
|
|
3
|
+
const DOT_PATH =
|
|
4
|
+
'M 806.90 322.97 C785.45,328.12 765.24,322.48 749.97,307.09 C737.88,294.91 733.00,282.92 733.00,265.40 C733.00,254.57 734.84,247.01 739.72,237.71 C746.39,225.02 759.41,213.62 772.68,208.85 C780.93,205.88 799.11,205.17 807.92,207.47 C830.44,213.34 847.16,231.12 851.67,253.99 C855.45,273.14 849.31,292.70 835.02,307.04 C826.17,315.92 818.73,320.14 806.90,322.97 Z'
|
|
5
|
+
|
|
6
|
+
export type AppLogoProps = {
|
|
7
|
+
/** Fill color for the A3 + ring (default: dark gray) */
|
|
8
|
+
markColor?: string
|
|
9
|
+
/** Fill color for the accent dot (default: orange) */
|
|
10
|
+
dotColor?: string
|
|
11
|
+
width?: number | string
|
|
12
|
+
height?: number | string
|
|
13
|
+
className?: string
|
|
14
|
+
'aria-label'?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_MARK = 'rgb(4,4,4)'
|
|
18
|
+
const DEFAULT_DOT = 'rgb(248,175,81)'
|
|
19
|
+
|
|
20
|
+
export function AppLogo({
|
|
21
|
+
markColor = DEFAULT_MARK,
|
|
22
|
+
dotColor = DEFAULT_DOT,
|
|
23
|
+
width = 32,
|
|
24
|
+
height = 32,
|
|
25
|
+
className,
|
|
26
|
+
'aria-label': ariaLabel = 'A3',
|
|
27
|
+
}: AppLogoProps) {
|
|
28
|
+
return (
|
|
29
|
+
<svg
|
|
30
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
31
|
+
viewBox="0 0 1024 1024"
|
|
32
|
+
width={width}
|
|
33
|
+
height={height}
|
|
34
|
+
className={className}
|
|
35
|
+
aria-label={ariaLabel}
|
|
36
|
+
role="img"
|
|
37
|
+
>
|
|
38
|
+
<g>
|
|
39
|
+
<path d={MARK_PATH} fill={markColor} />
|
|
40
|
+
<path d={DOT_PATH} fill={dotColor} />
|
|
41
|
+
</g>
|
|
42
|
+
</svg>
|
|
43
|
+
)
|
|
44
|
+
}
|