Skip to content

Commit bef31e0

Browse files
committed
feat: Introduce AgentRuntimeFactory and refactor agent turn handling
- Added AgentRuntimeFactory to encapsulate AgentRuntime creation logic. - Refactored simpleAgent.ts to streamline agent execution and remove deprecated code. - Implemented SpeechTurnService for handling speech input and response processing. - Created AgentToolProvider for managing API-based tools. - Introduced TurnLifecycleService and TurnPersistenceService for managing turn state and persistence. - Added TurnPromptBuilder for constructing prompts based on user input and context. - Implemented TurnStreamConsumer and VegaLiteStreamBuffer for processing streaming responses. - Updated turnTypes.ts to define new types for agent turn handling. - Refactored AgentTurnService to utilize new services and improve code organization.
1 parent e5015da commit bef31e0

18 files changed

Lines changed: 853 additions & 576 deletions

agent/middleware/apiBasedTools.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
} from "../toolCallEvents.js";
1212
import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
1313
import { createApiTool } from "../tools/apiTool.js";
14+
import type { AgentEventEmitter } from "../../agentEvents.js";
15+
import type { SequenceDebugCollector } from "./sequenceDebug.js";
1416

1517
function getEnabledApiToolNames(messages: unknown[]) {
1618
const enabledToolNames = new Set<string>();
@@ -80,11 +82,19 @@ export function createApiBasedToolsMiddleware(
8082
async wrapToolCall(request, handler) {
8183
const startedAt = Date.now();
8284
const toolInput = JSON.stringify(request.toolCall.args ?? {});
83-
const { adminUser, emitToolCallEvent, userTimeZone } = request.runtime.context as {
85+
const { adminUser, emit, sequenceDebugSink, userTimeZone } = request.runtime.context as {
8486
adminUser: AdminUser;
85-
emitToolCallEvent: ToolCallEventSink;
87+
emit?: AgentEventEmitter;
88+
sequenceDebugSink: SequenceDebugCollector;
8689
userTimeZone: string;
8790
};
91+
const emitToolCall: ToolCallEventSink = (event) => {
92+
sequenceDebugSink.handleToolCallEvent(event);
93+
void emit?.({
94+
type: "tool-call",
95+
data: event,
96+
});
97+
};
8898
const toolArgs = (request.toolCall.args ?? {}) as Record<string, unknown>;
8999
let toolInfo: string | undefined;
90100

@@ -102,7 +112,7 @@ export function createApiBasedToolsMiddleware(
102112
});
103113
}
104114
const toolCallTracker = createToolCallTracker({
105-
emit: emitToolCallEvent,
115+
emit: emitToolCall,
106116
toolCallId: request.toolCall.id,
107117
toolName: request.toolCall.name,
108118
toolInfo,

agent/models/AgentModeResolver.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { PluginOptions } from "../../types.js";
2+
3+
export class AgentModeResolver {
4+
constructor(private readonly options: PluginOptions) {}
5+
6+
resolve(modeName?: string | null) {
7+
return this.options.modes.find((mode) => mode.name === modeName) ?? this.options.modes[0];
8+
}
9+
}

agent/models/AgentModelFactory.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { CompletionAdapter } from "adminforth";
2+
import { createAgentChatModel } from "../simpleAgent.js";
3+
import type { AgentTurnModels } from "../turn/turnTypes.js";
4+
5+
export class AgentModelFactory {
6+
constructor(private readonly maxTokens: number) {}
7+
8+
async create(completionAdapter: CompletionAdapter): Promise<AgentTurnModels> {
9+
const [primaryModelSpec, summaryModelSpec] = await Promise.all([
10+
createAgentChatModel({
11+
adapter: completionAdapter,
12+
maxTokens: this.maxTokens,
13+
purpose: "primary",
14+
}),
15+
createAgentChatModel({
16+
adapter: completionAdapter,
17+
maxTokens: this.maxTokens,
18+
purpose: "summary",
19+
}),
20+
]);
21+
22+
return {
23+
model: primaryModelSpec.model,
24+
summaryModel: summaryModelSpec.model,
25+
modelMiddleware: primaryModelSpec.middleware,
26+
};
27+
}
28+
}

agent/runtime/AgentContext.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { AdminUser } from "adminforth";
2+
import { z } from "zod";
3+
import type { AgentEventEmitter } from "../../agentEvents.js";
4+
import type { SequenceDebugCollector } from "../middleware/sequenceDebug.js";
5+
import type { CurrentPageContext } from "../tools/getUserLocation.js";
6+
import type { AgentTurnContext } from "../turn/turnTypes.js";
7+
8+
export const contextSchema = z.object({
9+
adminUser: z.custom<AdminUser>(),
10+
userTimeZone: z.string(),
11+
sessionId: z.string(),
12+
turnId: z.string(),
13+
abortSignal: z.custom<AbortSignal>().optional(),
14+
currentPage: z.custom<CurrentPageContext>().optional(),
15+
chatSurface: z.string().optional(),
16+
adminBaseUrl: z.string().optional(),
17+
adminPublicOrigin: z.string().optional(),
18+
emit: z.custom<AgentEventEmitter>().optional(),
19+
sequenceDebugSink: z.custom<SequenceDebugCollector>(),
20+
});
21+
22+
export function toLangchainAgentContext(
23+
context: AgentTurnContext & {
24+
adminBaseUrl: string;
25+
emit?: AgentEventEmitter;
26+
sequenceDebugSink: SequenceDebugCollector;
27+
},
28+
) {
29+
return context;
30+
}

agent/runtime/AgentModels.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type {
2+
AgentChatModel,
3+
AgentModelPurpose,
4+
AgentModeCompletionAdapter,
5+
} from "../simpleAgent.js";

agent/runtime/AgentRuntime.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { IAdminForth } from "adminforth";
2+
import { createAgent, summarizationMiddleware } from "langchain";
3+
import type { BaseCheckpointSaver } from "@langchain/langgraph";
4+
import { createApiBasedToolsMiddleware } from "../middleware/apiBasedTools.js";
5+
import { createSequenceDebugMiddleware } from "../middleware/sequenceDebug.js";
6+
import { createAgentLlmMetricsLogger } from "../simpleAgent.js";
7+
import type { AgentToolProvider } from "../tools/AgentToolProvider.js";
8+
import type { AgentRuntimeRunInput } from "../turn/turnTypes.js";
9+
import { contextSchema, toLangchainAgentContext } from "./AgentContext.js";
10+
11+
export type AgentRuntimeOptions = {
12+
name: string;
13+
adminforth: IAdminForth;
14+
checkpointer: BaseCheckpointSaver;
15+
toolProvider: AgentToolProvider;
16+
};
17+
18+
export class AgentRuntime {
19+
constructor(private readonly options: AgentRuntimeOptions) {}
20+
21+
async stream(input: AgentRuntimeRunInput) {
22+
const tools = await this.options.toolProvider.getTools();
23+
const apiBasedTools = this.options.toolProvider.getApiBasedTools();
24+
const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(
25+
apiBasedTools,
26+
this.options.adminforth,
27+
);
28+
const sequenceDebugMiddleware = createSequenceDebugMiddleware(
29+
input.observability.sequenceDebugSink,
30+
);
31+
const middleware = [
32+
apiBasedToolsMiddleware,
33+
...(input.models.modelMiddleware ?? []),
34+
sequenceDebugMiddleware,
35+
summarizationMiddleware({
36+
model: input.models.summaryModel,
37+
trigger: { tokens: 1024 * 64 },
38+
keep: { messages: 10 },
39+
}),
40+
] as const;
41+
42+
const agent = createAgent({
43+
name: this.options.name,
44+
model: input.models.model,
45+
checkpointer: this.options.checkpointer,
46+
tools,
47+
contextSchema,
48+
middleware,
49+
});
50+
51+
return agent.stream({ messages: input.messages } as any, {
52+
streamMode: "messages",
53+
recursionLimit: 100,
54+
callbacks: [createAgentLlmMetricsLogger()],
55+
signal: input.context.abortSignal,
56+
configurable: {
57+
thread_id: input.context.sessionId,
58+
},
59+
context: toLangchainAgentContext({
60+
...input.context,
61+
adminBaseUrl: this.options.adminforth.config.baseUrlSlashed,
62+
emit: input.observability.emit,
63+
sequenceDebugSink: input.observability.sequenceDebugSink,
64+
}),
65+
});
66+
}
67+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { IAdminForth } from "adminforth";
2+
import type { BaseCheckpointSaver } from "@langchain/langgraph";
3+
import { AgentRuntime } from "./AgentRuntime.js";
4+
import type { AgentToolProvider } from "../tools/AgentToolProvider.js";
5+
6+
export class AgentRuntimeFactory {
7+
constructor(
8+
private readonly getAdminforth: () => IAdminForth,
9+
private readonly getCheckpointer: () => BaseCheckpointSaver,
10+
private readonly toolProvider: AgentToolProvider,
11+
private readonly getName: () => string,
12+
) {}
13+
14+
create() {
15+
return new AgentRuntime({
16+
name: this.getName(),
17+
adminforth: this.getAdminforth(),
18+
checkpointer: this.getCheckpointer(),
19+
toolProvider: this.toolProvider,
20+
});
21+
}
22+
}

agent/simpleAgent.ts

Lines changed: 2 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,13 @@
11
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
2-
import { createAgent, summarizationMiddleware } from "langchain";
32
import {
43
logger,
5-
type AdminUser,
64
type CompletionAdapter,
7-
type IAdminForth,
85
} from "adminforth";
96
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
10-
import {type BaseCheckpointSaver, type Messages } from "@langchain/langgraph";
117
import type { LLMResult } from "@langchain/core/outputs";
12-
import { z } from "zod";
13-
import { createAgentTools } from "./tools/index.js";
14-
import { createApiBasedToolsMiddleware } from "./middleware/apiBasedTools.js";
158
import {
169
createSequenceDebugMiddleware,
17-
type SequenceDebugModelCallSink,
1810
} from "./middleware/sequenceDebug.js";
19-
import type { ApiBasedTool } from "../apiBasedTools.js";
20-
import type { ToolCallEventSink } from "./toolCallEvents.js";
21-
import type { CurrentPageContext } from "./tools/getUserLocation.js";
22-
import type { AgentEventEmitter } from "../agentEvents.js";
23-
24-
export const contextSchema = z.object({
25-
adminUser: z.custom<AdminUser>(),
26-
userTimeZone: z.string(),
27-
sessionId: z.string(),
28-
turnId: z.string(),
29-
abortSignal: z.custom<AbortSignal>().optional(),
30-
currentPage: z.custom<CurrentPageContext>().optional(),
31-
chatSurface: z.string().optional(),
32-
adminBaseUrl: z.string().optional(),
33-
adminPublicOrigin: z.string().optional(),
34-
emitToolCallEvent: z.custom<ToolCallEventSink>(),
35-
emitAgentEvent: z.custom<AgentEventEmitter>().optional(),
36-
});
3711

3812
export type AgentChatModel = BaseChatModel<any, any>;
3913
export type AgentModelPurpose = "primary" | "summary";
@@ -50,7 +24,7 @@ export type AgentModeCompletionAdapter = CompletionAdapter & {
5024
};
5125
};
5226

53-
type AgentMiddleware = ReturnType<typeof createSequenceDebugMiddleware>;
27+
export type AgentMiddleware = ReturnType<typeof createSequenceDebugMiddleware>;
5428

5529
type AgentChatModelSpec = {
5630
model: AgentChatModel;
@@ -202,7 +176,7 @@ class AgentLlmMetricsLogger extends BaseCallbackHandler {
202176
}
203177
}
204178

205-
function createAgentLlmMetricsLogger() {
179+
export function createAgentLlmMetricsLogger() {
206180
return new AgentLlmMetricsLogger();
207181
}
208182

@@ -223,104 +197,3 @@ export async function createAgentChatModel(params: {
223197
purpose: params.purpose,
224198
});
225199
}
226-
227-
export async function callAgent(params: {
228-
name: string;
229-
model: AgentChatModel;
230-
summaryModel: AgentChatModel;
231-
modelMiddleware?: AgentMiddleware[];
232-
checkpointer?: BaseCheckpointSaver;
233-
messages: Messages;
234-
adminUser: AdminUser;
235-
adminforth: IAdminForth;
236-
apiBasedTools: Record<string, ApiBasedTool>;
237-
customComponentsDir: string;
238-
pluginCustomFolderPaths: string[];
239-
sessionId: string;
240-
turnId: string;
241-
currentPage?: CurrentPageContext;
242-
chatSurface?: string;
243-
adminPublicOrigin?: string;
244-
userTimeZone: string;
245-
abortSignal?: AbortSignal;
246-
emitToolCallEvent: ToolCallEventSink;
247-
emitAgentEvent?: AgentEventEmitter;
248-
sequenceDebugSink: SequenceDebugModelCallSink;
249-
}) {
250-
const {
251-
name,
252-
model,
253-
summaryModel,
254-
modelMiddleware = [],
255-
checkpointer,
256-
messages,
257-
adminUser,
258-
adminforth,
259-
apiBasedTools,
260-
customComponentsDir,
261-
pluginCustomFolderPaths,
262-
sessionId,
263-
turnId,
264-
currentPage,
265-
chatSurface,
266-
adminPublicOrigin,
267-
userTimeZone,
268-
abortSignal,
269-
emitToolCallEvent,
270-
emitAgentEvent,
271-
sequenceDebugSink,
272-
} = params;
273-
274-
const tools = await createAgentTools(
275-
customComponentsDir,
276-
apiBasedTools,
277-
pluginCustomFolderPaths,
278-
);
279-
const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools, adminforth);
280-
const sequenceDebugMiddleware = createSequenceDebugMiddleware(
281-
sequenceDebugSink,
282-
);
283-
284-
const middleware = [
285-
apiBasedToolsMiddleware,
286-
...modelMiddleware,
287-
sequenceDebugMiddleware,
288-
summarizationMiddleware({
289-
model: summaryModel,
290-
trigger: { tokens: 1024 * 64 },
291-
keep: { messages: 10 },
292-
}),
293-
] as const;
294-
295-
const agent = createAgent<undefined, typeof contextSchema, typeof middleware>({
296-
name,
297-
model,
298-
checkpointer,
299-
tools,
300-
contextSchema,
301-
middleware,
302-
});
303-
304-
return await agent.stream({ messages } as any, {
305-
streamMode: "messages",
306-
recursionLimit: 100,
307-
callbacks: [createAgentLlmMetricsLogger()],
308-
signal: abortSignal,
309-
configurable: {
310-
thread_id: sessionId,
311-
},
312-
context: {
313-
adminUser,
314-
userTimeZone,
315-
sessionId,
316-
turnId,
317-
abortSignal,
318-
currentPage,
319-
chatSurface,
320-
adminBaseUrl: adminforth.config.baseUrlSlashed,
321-
adminPublicOrigin,
322-
emitToolCallEvent,
323-
emitAgentEvent,
324-
},
325-
});
326-
}

0 commit comments

Comments
 (0)