Skip to content

Commit a6ff330

Browse files
authored
Add classes for BaseAgent and RootAgent + add Agent Streamer (#2922)
* feat: Add classes for BaseAgent and RootAgent
1 parent a189467 commit a6ff330

File tree

9 files changed

+190
-64
lines changed

9 files changed

+190
-64
lines changed

apps/web/client/src/app/api/chat/route.ts

Lines changed: 36 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { api } from '@/trpc/server';
22
import { trackEvent } from '@/utils/analytics/server';
3-
import { convertToStreamMessages, getToolSetFromType } from '@onlook/ai';
3+
import { AgentStreamer, RootAgent } from '@onlook/ai';
44
import { toDbMessage } from '@onlook/db';
5-
import { ChatType, type ChatMessage, type ChatMetadata } from '@onlook/models';
6-
import { stepCountIs, streamText } from 'ai';
5+
import { ChatType, type ChatMessage } from '@onlook/models';
76
import { type NextRequest } from 'next/server';
87
import { v4 as uuidv4 } from 'uuid';
9-
import { checkMessageLimit, decrementUsage, errorHandler, getModelFromType, getSupabaseUser, getSystemPromptFromType, incrementUsage, repairToolCall } from './helpers';
10-
11-
const MAX_STEPS = 20;
8+
import { checkMessageLimit, decrementUsage, errorHandler, getSupabaseUser, incrementUsage, repairToolCall } from './helpers';
129

1310
export async function POST(req: NextRequest) {
1411
try {
@@ -77,65 +74,41 @@ export const streamResponse = async (req: NextRequest, userId: string) => {
7774
if (chatType === ChatType.EDIT) {
7875
usageRecord = await incrementUsage(req, traceId);
7976
}
80-
const modelConfig = await getModelFromType(chatType);
81-
const { model, providerOptions, headers } = modelConfig;
82-
const systemPrompt = getSystemPromptFromType(chatType);
83-
const tools = getToolSetFromType(chatType);
84-
const result = streamText({
85-
model,
86-
headers,
87-
tools,
88-
stopWhen: stepCountIs(MAX_STEPS),
89-
messages: [
90-
{
91-
role: 'system',
92-
content: systemPrompt,
93-
providerOptions,
94-
},
95-
...convertToStreamMessages(messages),
96-
],
97-
experimental_telemetry: {
98-
isEnabled: true,
99-
metadata: {
100-
conversationId,
101-
projectId,
102-
userId,
103-
chatType: chatType,
104-
tags: ['chat'],
105-
langfuseTraceId: traceId,
106-
sessionId: conversationId,
107-
},
108-
},
109-
experimental_repairToolCall: repairToolCall,
110-
onError: async (error) => {
111-
console.error('Error in chat stream call', error);
112-
// if there was an error with the API, do not penalize the user
113-
await decrementUsage(req, usageRecord);
11477

115-
// Ensure the stream stops on error by re-throwing
116-
if (error instanceof Error) {
117-
throw error;
118-
} else {
119-
const errorMessage = typeof error === 'string' ? error : JSON.stringify(error);
120-
throw new Error(errorMessage);
121-
}
122-
}
123-
})
78+
// Create RootAgent instance
79+
const agent = await RootAgent.create(chatType);
80+
const streamer = new AgentStreamer(agent, conversationId);
12481

125-
return result.toUIMessageStreamResponse<ChatMessage>(
126-
{
127-
originalMessages: messages,
128-
generateMessageId: () => uuidv4(),
129-
messageMetadata: ({ part }) => {
130-
return {
131-
createdAt: new Date(),
82+
return streamer.streamText(messages, {
83+
streamTextConfig: {
84+
experimental_telemetry: {
85+
isEnabled: true,
86+
metadata: {
13287
conversationId,
133-
context: [],
134-
checkpoints: [],
135-
finishReason: part.type === 'finish-step' ? part.finishReason : undefined,
136-
usage: part.type === 'finish-step' ? part.usage : undefined,
137-
} satisfies ChatMetadata;
88+
projectId,
89+
userId,
90+
chatType: chatType,
91+
tags: ['chat'],
92+
langfuseTraceId: traceId,
93+
sessionId: conversationId,
94+
},
13895
},
96+
experimental_repairToolCall: repairToolCall,
97+
onError: async (error) => {
98+
console.error('Error in chat stream call', error);
99+
// if there was an error with the API, do not penalize the user
100+
await decrementUsage(req, usageRecord);
101+
102+
// Ensure the stream stops on error by re-throwing
103+
if (error instanceof Error) {
104+
throw error;
105+
} else {
106+
const errorMessage = typeof error === 'string' ? error : JSON.stringify(error);
107+
throw new Error(errorMessage);
108+
}
109+
},
110+
},
111+
toUIMessageStreamResponseConfig: {
139112
onFinish: async ({ messages: finalMessages }) => {
140113
const messagesToStore = finalMessages
141114
.filter(msg =>
@@ -149,8 +122,8 @@ export const streamResponse = async (req: NextRequest, userId: string) => {
149122
});
150123
},
151124
onError: errorHandler,
152-
}
153-
);
125+
},
126+
});
154127
} catch (error) {
155128
console.error('Error in streamResponse setup', error);
156129
// If there was an error setting up the stream and we incremented usage, revert it

packages/ai/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"marked": "^15.0.7",
4949
"openai": "^4.103.0",
5050
"zod": "^4.1.3",
51-
"@onlook/ui": "*"
51+
"@onlook/ui": "*",
52+
"uuid": "^11.1.0"
5253
}
5354
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { RootAgent } from './root';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ChatType, LLMProvider, OPENROUTER_MODELS, type ModelConfig } from '@onlook/models';
2+
import { initModel } from '../../chat/providers';
3+
import { getAskModeSystemPrompt, getCreatePageSystemPrompt, getSystemPrompt } from '../../prompt';
4+
import { getToolSetFromType } from '../../tools/toolset';
5+
import { BaseAgent } from '../models/base';
6+
7+
export class RootAgent extends BaseAgent {
8+
readonly id = 'root-agent';
9+
private readonly chatType: ChatType;
10+
readonly modelConfig: ModelConfig;
11+
12+
constructor(chatType: ChatType, modelConfig: ModelConfig) {
13+
super(getToolSetFromType(chatType));
14+
15+
this.chatType = chatType;
16+
this.modelConfig = modelConfig;
17+
}
18+
19+
get systemPrompt(): string {
20+
return this.getSystemPromptFromType(this.chatType);
21+
}
22+
23+
private getSystemPromptFromType(chatType: ChatType): string {
24+
switch (chatType) {
25+
case ChatType.CREATE:
26+
return getCreatePageSystemPrompt();
27+
case ChatType.ASK:
28+
return getAskModeSystemPrompt();
29+
case ChatType.EDIT:
30+
default:
31+
return getSystemPrompt();
32+
}
33+
}
34+
35+
static async create(chatType: ChatType): Promise<RootAgent> {
36+
const modelConfig = await RootAgent.getModelFromType(chatType);
37+
return new RootAgent(chatType, modelConfig);
38+
}
39+
40+
private static async getModelFromType(chatType: ChatType): Promise<ModelConfig> {
41+
switch (chatType) {
42+
case ChatType.CREATE:
43+
case ChatType.FIX:
44+
return await initModel({
45+
provider: LLMProvider.OPENROUTER,
46+
model: OPENROUTER_MODELS.OPEN_AI_GPT_5,
47+
});
48+
case ChatType.ASK:
49+
case ChatType.EDIT:
50+
default:
51+
return await initModel({
52+
provider: LLMProvider.OPENROUTER,
53+
model: OPENROUTER_MODELS.CLAUDE_4_SONNET,
54+
});
55+
}
56+
}
57+
}

packages/ai/src/agents/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './models';
2+
export * from './classes';
3+
export { AgentStreamer } from './streamer';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { ModelConfig } from '@onlook/models';
2+
import { stepCountIs, streamText, type ModelMessage, type ToolSet } from 'ai';
3+
4+
export abstract class BaseAgent {
5+
abstract readonly id: string;
6+
abstract readonly modelConfig: ModelConfig;
7+
abstract readonly systemPrompt: string;
8+
protected readonly toolSet: ToolSet
9+
10+
constructor(toolSet: ToolSet = {}) {
11+
this.toolSet = toolSet;
12+
}
13+
14+
getSystemPrompt() {
15+
return this.systemPrompt;
16+
}
17+
18+
getToolSet() {
19+
return this.toolSet;
20+
}
21+
22+
// Default streamText configuration - can be overridden by subclasses
23+
protected getStreamTextConfig() {
24+
return {
25+
maxSteps: 20,
26+
};
27+
}
28+
29+
// Main streamText method that uses the agent's configuration
30+
streamText(
31+
messages: ModelMessage[] = [],
32+
additionalConfig: Omit<Partial<Parameters<typeof streamText>[0]>, 'model' | 'headers' | 'tools' | 'stopWhen' | 'messages' | 'prompt'> = {},
33+
) {
34+
const config = this.getStreamTextConfig();
35+
36+
return streamText({
37+
model: this.modelConfig.model,
38+
headers: this.modelConfig.headers,
39+
tools: this.getToolSet(),
40+
stopWhen: stepCountIs(config.maxSteps),
41+
messages: [
42+
{
43+
role: 'system',
44+
content: this.getSystemPrompt(),
45+
providerOptions: this.modelConfig.providerOptions,
46+
},
47+
...messages
48+
],
49+
...additionalConfig,
50+
})
51+
}
52+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BaseAgent } from './base';

packages/ai/src/agents/streamer.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ChatMessage, ChatMetadata } from "@onlook/models";
2+
import type { BaseAgent } from "./models";
3+
import { convertToStreamMessages } from "../stream";
4+
import { v4 as uuidv4 } from 'uuid';
5+
import type { streamText, UIMessageStreamOnFinishCallback, UIMessageStreamOptions } from "ai";
6+
7+
export class AgentStreamer {
8+
private readonly agent: BaseAgent;
9+
private readonly conversationId: string;
10+
11+
constructor(agent: BaseAgent, conversationId: string) {
12+
this.agent = agent;
13+
this.conversationId = conversationId;
14+
}
15+
16+
async streamText(messages: ChatMessage[], { streamTextConfig, toUIMessageStreamResponseConfig }: { streamTextConfig: Omit<Partial<Parameters<typeof streamText>[0]>, 'model' | 'headers' | 'tools' | 'stopWhen' | 'messages' | 'prompt'>, toUIMessageStreamResponseConfig: UIMessageStreamOptions<ChatMessage> }) {
17+
const conversationId = this.conversationId;
18+
19+
const result = await this.agent.streamText(convertToStreamMessages(messages), streamTextConfig);
20+
21+
return result.toUIMessageStreamResponse<ChatMessage>({
22+
originalMessages: messages,
23+
generateMessageId: () => uuidv4(),
24+
messageMetadata: ({ part }) => {
25+
return {
26+
createdAt: new Date(),
27+
conversationId,
28+
context: [],
29+
checkpoints: [],
30+
finishReason: part.type === 'finish-step' ? part.finishReason : undefined,
31+
usage: part.type === 'finish-step' ? part.usage : undefined,
32+
} satisfies ChatMetadata;
33+
},
34+
...toUIMessageStreamResponseConfig,
35+
});
36+
}
37+
}

packages/ai/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './agents';
12
export * from './apply';
23
export * from './chat';
34
export * from './prompt';

0 commit comments

Comments
 (0)