From a321e2f9baf95e9b4baebd8a9e648100e2d7a4b4 Mon Sep 17 00:00:00 2001 From: Ryan Sweet Date: Sun, 23 Feb 2025 14:49:29 -0800 Subject: [PATCH] add GettingStarted for typescript --- typescript/contracts/IAgentRuntime.ts | 7 ++- typescript/core/AgentsApp.ts | 27 +++++++++ .../core/TypePrefixSubscriptionAttribute.ts | 13 ++++- typescript/core/TypeSubscription.ts | 4 +- typescript/core/TypeSubscriptionAttribute.ts | 13 ++++- typescript/samples/GettingStarted/Checker.ts | 40 +++++++++++++ .../samples/GettingStarted/CountMessage.ts | 3 + .../samples/GettingStarted/CountUpdate.ts | 3 + typescript/samples/GettingStarted/Modifier.ts | 30 ++++++++++ typescript/samples/GettingStarted/index.ts | 56 +++++++++++++++++++ .../samples/GettingStarted/package.json | 15 +++++ typescript/tsconfig.json | 4 +- 12 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 typescript/samples/GettingStarted/Checker.ts create mode 100644 typescript/samples/GettingStarted/CountMessage.ts create mode 100644 typescript/samples/GettingStarted/CountUpdate.ts create mode 100644 typescript/samples/GettingStarted/Modifier.ts create mode 100644 typescript/samples/GettingStarted/index.ts create mode 100644 typescript/samples/GettingStarted/package.json diff --git a/typescript/contracts/IAgentRuntime.ts b/typescript/contracts/IAgentRuntime.ts index cdf7c6691..cfb29319d 100644 --- a/typescript/contracts/IAgentRuntime.ts +++ b/typescript/contracts/IAgentRuntime.ts @@ -28,5 +28,10 @@ export interface IAgentRuntime { loadAgentStateAsync(agentId: AgentId, state: unknown): Promise; saveAgentStateAsync(agentId: AgentId): Promise; - // ...additional methods from the .NET interface... + registerAgentFactoryAsync( + type: AgentType, + factoryFunc: (agentId: AgentId, runtime: IAgentRuntime) => Promise + ): Promise; + + addSubscriptionAsync(subscription: ISubscriptionDefinition): Promise; } \ No newline at end of file diff --git a/typescript/core/AgentsApp.ts b/typescript/core/AgentsApp.ts index 91a01ab39..11010a1d3 100644 --- a/typescript/core/AgentsApp.ts +++ b/typescript/core/AgentsApp.ts @@ -31,6 +31,7 @@ export class AgentsAppBuilder { export class AgentsApp { private running = false; + private shutdownCallbacks: Array<() => void> = []; constructor(public readonly runtime: IAgentRuntime) {} @@ -60,4 +61,30 @@ export class AgentsApp { } await this.runtime.publishMessageAsync(message, topic, undefined, messageId); } + + async publishMessageAsync( + message: T, + topic: TopicId, + messageId?: string + ): Promise { + if (!this.running) { + await this.start(); + } + await this.runtime.publishMessageAsync(message, topic, undefined, messageId); + } + + async waitForShutdown(): Promise { + // Wait until shutdown is called + return new Promise((resolve) => { + if (!this.running) { + resolve(); + } else { + const cleanup = () => { + this.running = false; + resolve(); + }; + this.shutdownCallbacks.push(cleanup); + } + }); + } } diff --git a/typescript/core/TypePrefixSubscriptionAttribute.ts b/typescript/core/TypePrefixSubscriptionAttribute.ts index 42895374b..6539f8edc 100644 --- a/typescript/core/TypePrefixSubscriptionAttribute.ts +++ b/typescript/core/TypePrefixSubscriptionAttribute.ts @@ -1,12 +1,21 @@ +import "reflect-metadata"; import { ISubscriptionDefinition } from "../contracts/ISubscriptionDefinition"; import { IUnboundSubscriptionDefinition } from "../contracts/IUnboundSubscriptionDefinition"; import { AgentType } from "../contracts/AgentType"; -import { TypePrefixSubscription } from "./TypePrefixSubscription"; +import { TypePrefixSubscription as TypePrefixSubscriptionImpl } from "./TypePrefixSubscription"; + +// Export the decorator +export function TypePrefixSubscription(topic: string): ClassDecorator { + return function (target: Function): void { + // Store subscription metadata on the class + (Reflect as any).defineMetadata('subscription:topic:prefix', topic, target); + }; +} export class TypePrefixSubscriptionAttribute implements IUnboundSubscriptionDefinition { constructor(private readonly topic: string) {} bind(agentType: AgentType): ISubscriptionDefinition { - return new TypePrefixSubscription(this.topic, agentType); + return new TypePrefixSubscriptionImpl(this.topic, agentType); } } diff --git a/typescript/core/TypeSubscription.ts b/typescript/core/TypeSubscription.ts index d6662e29c..40909661d 100644 --- a/typescript/core/TypeSubscription.ts +++ b/typescript/core/TypeSubscription.ts @@ -7,9 +7,9 @@ export class TypeSubscription implements ISubscriptionDefinition { private readonly agentType: AgentType; public readonly id: string; - constructor(topicType: string, agentType: AgentType, id?: string) { + constructor(topicType: string, agentType?: AgentType, id?: string) { this.topicType = topicType; - this.agentType = agentType; + this.agentType = agentType || topicType; this.id = id ?? crypto.randomUUID(); } diff --git a/typescript/core/TypeSubscriptionAttribute.ts b/typescript/core/TypeSubscriptionAttribute.ts index 101377dbd..ae235a7f1 100644 --- a/typescript/core/TypeSubscriptionAttribute.ts +++ b/typescript/core/TypeSubscriptionAttribute.ts @@ -1,12 +1,21 @@ +import 'reflect-metadata'; import { ISubscriptionDefinition } from "../contracts/ISubscriptionDefinition"; import { IUnboundSubscriptionDefinition } from "../contracts/IUnboundSubscriptionDefinition"; import { AgentType } from "../contracts/AgentType"; -import { TypeSubscription } from "./TypeSubscription"; +import { TypeSubscription as TypeSubscriptionImpl } from "./TypeSubscription"; + +// Export the decorator +export function TypeSubscription(topic: string): ClassDecorator { + return function (target: Function): void { + // Store subscription metadata on the class + (Reflect as any).defineMetadata('subscription:topic', topic, target); + }; +} export class TypeSubscriptionAttribute implements IUnboundSubscriptionDefinition { constructor(private readonly topic: string) {} bind(agentType: AgentType): ISubscriptionDefinition { - return new TypeSubscription(this.topic, agentType); + return new TypeSubscriptionImpl(this.topic, agentType); } } diff --git a/typescript/samples/GettingStarted/Checker.ts b/typescript/samples/GettingStarted/Checker.ts new file mode 100644 index 000000000..89ace009a --- /dev/null +++ b/typescript/samples/GettingStarted/Checker.ts @@ -0,0 +1,40 @@ +import { BaseAgent } from "../../core/BaseAgent"; +import { IHandle } from "../../contracts/IHandle"; +import { AgentId, IAgentRuntime } from "../../contracts/IAgentRuntime"; +import { MessageContext } from "../../contracts/MessageContext"; +import { TypeSubscription } from "../../core/TypeSubscriptionAttribute"; +import { CountMessage } from "./CountMessage"; +import { CountUpdate } from "./CountUpdate"; + +type TerminationF = (x: number) => boolean; + +@TypeSubscription("default") +export class Checker extends BaseAgent implements IHandle { + private runUntilFunc: TerminationF; + private shutdown: () => void; + + constructor( + id: AgentId, + runtime: IAgentRuntime, + runUntilFunc: TerminationF, + shutdown: () => void + ) { + super(id, runtime, "Checker"); + this.runUntilFunc = runUntilFunc; + this.shutdown = shutdown; + } + + async handleAsync(message: CountUpdate, context: MessageContext): Promise { + if (!this.runUntilFunc(message.newCount)) { + console.log(`\nChecker:\n${message.newCount} passed the check, continue.`); + const countMessage: CountMessage = { content: message.newCount }; + await this.runtime.publishMessageAsync( + countMessage, + { type: "default", source: this.id.key } + ); + } else { + console.log(`\nChecker:\n${message.newCount} failed the check, stopping.`); + this.shutdown(); + } + } +} diff --git a/typescript/samples/GettingStarted/CountMessage.ts b/typescript/samples/GettingStarted/CountMessage.ts new file mode 100644 index 000000000..33b52df40 --- /dev/null +++ b/typescript/samples/GettingStarted/CountMessage.ts @@ -0,0 +1,3 @@ +export interface CountMessage { + content: number; +} \ No newline at end of file diff --git a/typescript/samples/GettingStarted/CountUpdate.ts b/typescript/samples/GettingStarted/CountUpdate.ts new file mode 100644 index 000000000..baea0f45e --- /dev/null +++ b/typescript/samples/GettingStarted/CountUpdate.ts @@ -0,0 +1,3 @@ +export interface CountUpdate { + newCount: number; +} \ No newline at end of file diff --git a/typescript/samples/GettingStarted/Modifier.ts b/typescript/samples/GettingStarted/Modifier.ts new file mode 100644 index 000000000..02c3fc720 --- /dev/null +++ b/typescript/samples/GettingStarted/Modifier.ts @@ -0,0 +1,30 @@ +import { BaseAgent } from "../../core/BaseAgent"; +import { IHandle } from "../../contracts/IHandle"; +import { AgentId, IAgentRuntime } from "../../contracts/IAgentRuntime"; +import { MessageContext } from "../../contracts/MessageContext"; +import { TypeSubscription } from "../../core/TypeSubscriptionAttribute"; +import { CountMessage } from "./CountMessage"; +import { CountUpdate } from "./CountUpdate"; + +type ModifyF = (x: number) => number; + +@TypeSubscription("default") +export class Modifier extends BaseAgent implements IHandle { + private modifyFunc: ModifyF; + + constructor(id: AgentId, runtime: IAgentRuntime, modifyFunc: ModifyF) { + super(id, runtime, "Modifier"); + this.modifyFunc = modifyFunc; + } + + async handleAsync(message: CountMessage, context: MessageContext): Promise { + const newValue = this.modifyFunc(message.content); + console.log(`\nModifier:\nModified ${message.content} to ${newValue}`); + + const updateMessage: CountUpdate = { newCount: newValue }; + await this.runtime.publishMessageAsync( + updateMessage, + { type: "default", source: this.id.key } + ); + } +} diff --git a/typescript/samples/GettingStarted/index.ts b/typescript/samples/GettingStarted/index.ts new file mode 100644 index 000000000..154c2bc66 --- /dev/null +++ b/typescript/samples/GettingStarted/index.ts @@ -0,0 +1,56 @@ +import "reflect-metadata"; +import { AgentsApp, AgentsAppBuilder } from "../../core/AgentsApp"; +import { Checker } from "./Checker"; +import { Modifier } from "./Modifier"; +import { CountMessage } from "./CountMessage"; +import { TypeSubscription } from "../../core/TypeSubscription"; +import { AgentId, IAgentRuntime } from "../../contracts/IAgentRuntime"; + +async function main() { + // Define the modification and termination functions + const modifyFunc = (x: number) => x - 1; + const runUntilFunc = (x: number) => x <= 1; + + // Create and configure the app + const appBuilder = new AgentsAppBuilder() + .useInProcessRuntime(false); // Set deliverToSelf to false + + const app = await appBuilder.build(); + + // Setup shutdown handler + const shutdown = () => { + app.shutdown(); + }; + + // Register agents + await app.runtime.registerAgentFactoryAsync("Checker", + async (id: AgentId, runtime: IAgentRuntime) => + new Checker(id, runtime, runUntilFunc, shutdown)); + + await app.runtime.registerAgentFactoryAsync("Modifier", + async (id: AgentId, runtime: IAgentRuntime) => + new Modifier(id, runtime, modifyFunc)); + + // Add subscriptions + await app.runtime.addSubscriptionAsync( + new TypeSubscription("default") + ); + await app.runtime.addSubscriptionAsync( + new TypeSubscription("default") + ); + + // Start the app + await app.start(); + + // Send initial message + await app.publishMessageAsync( + { content: 10 }, + { type: "default", source: "main" } + ); + + // Wait for shutdown + await app.waitForShutdown(); +} + +// Run the program +main().catch(console.error); diff --git a/typescript/samples/GettingStarted/package.json b/typescript/samples/GettingStarted/package.json new file mode 100644 index 000000000..1911032e8 --- /dev/null +++ b/typescript/samples/GettingStarted/package.json @@ -0,0 +1,15 @@ +{ + "name": "autogen-getting-started", + "version": "1.0.0", + "description": "AutoGen Getting Started Sample", + "main": "index.js", + "scripts": { + "start": "ts-node index.ts" + }, + "dependencies": { + "typescript": "^5.0.0", + "ts-node": "^10.9.0", + "reflect-metadata": "^0.1.13", + "@types/reflect-metadata": "^0.1.0" + } +} diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json index 82cf1186a..7ea3ab2d0 100644 --- a/typescript/tsconfig.json +++ b/typescript/tsconfig.json @@ -6,7 +6,9 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "outDir": "dist" + "outDir": "dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": [ "."