Skip to content

Commit 9c1e4f5

Browse files
btspoonywtfsayocoderabbitai[bot]
authored
feat: introduce Dependency Injection to enhance developer experience (elizaOS#2115)
* feat: Add @elizaos/plugin-di package with initial implementation - Created a new plugin package `@elizaos/plugin-di` to extend the Eliza platform. - Added essential files including `package.json`, `README.md`, and TypeScript configuration. - Implemented core functionalities such as dependency injection using Inversify, action handling, and content evaluation. - Introduced decorators for content properties and Zod schema integration. - Included sample actions and evaluators to demonstrate plugin capabilities. - Configured build and linting scripts for development workflow. * feat: add tests for content decorators in plugin-di - Introduced a new TypeScript configuration file `tsconfig.build.json` for build-specific settings. - Updated `tsconfig.json` to enable experimental decorators and metadata emission. - Added unit tests for content decorators, validating schema creation and property description loading. - Implemented tests for the `property` decorator to ensure metadata storage functionality. * feat: implement base injectable action class in plugin-di - Removed export of providers from index.ts. - Added a new file `baseInjectableAction.ts` that defines the `BaseInjactableAction` class, which serves as an abstract base for creating injectable actions. - Introduced action options and methods for processing messages, validating content, and handling actions within the Eliza framework. - Updated `index.ts` in actions directory to export the new base class. * fix: correct export typo and refactor evaluators in plugin-di - Fixed a typo in the export statement from `evaluators` to `evalutors` in `index.ts`. - Added a new file `baseInjectableEvalutor.ts` to define the base class for injectable evaluators. - Removed obsolete sample action and evaluator files to streamline the codebase. - Updated `types.ts` to include the `ActionOptions` type definition for better action handling. * feat: enhance plugin-di with new evaluators and action examples - Fixed export typo in `index.ts` from `evalutors` to `evaluators`. - Added `EvaluatorOptions` type definition in `types.ts` for better evaluator configuration. - Introduced new sample action and evaluator files to demonstrate usage within the plugin. - Implemented a base abstract class for injectable evaluators in `baseInjactableEvaluator.ts`. - Updated `createPlugin` function to handle actions more effectively. - Removed obsolete `index.ts` from the `evalutors` directory to clean up the codebase. * feat: update CreateResource action to handle nullable content and refactor structure - Modified the `execute` method in `BaseInjactableAction` and `CreateResourceAction` to accept nullable content. - Introduced a new `CreateResourceContent` class to define the structure of the content for resource creation. - Refactored the `CreateResourceAction` to utilize the new content class and improved validation logic. - Enhanced the callback responses to handle cases where no content is provided, ensuring better error handling. * feat: enhance plugin-di with new sample action, evaluator, provider, and plugin integration - Introduced `CreateResourceAction` to manage resource creation with dependency injection. - Added `SampleEvaluator` for evaluating important content in memory, implementing the `BaseInjactableEvaluator`. - Created `SampleProvider` for data retrieval, adhering to the `InjectableProvider` interface. - Registered new components with the global container for dependency injection. - Defined a `samplePlugin` to encapsulate the new action, evaluator, and provider, facilitating resource management. * refactor: update dependency injection for actions and providers - Changed `CreateResourceAction` and `SampleEvaluator` to use `inRequestScope()` for better lifecycle management. - Introduced dynamic data binding in `SampleProvider` with `toDynamicValue()` for asynchronous data retrieval. - Updated `SampleProvider` to inject dynamic data and modified the `get` method to return a string representation of the shared and dynamic data. - Adjusted import statements in `baseInjectableAction.ts` and `baseInjactableEvaluator.ts` to use `import type` for improved type safety. * fix: improve null safety in normalizeCharacter function - Updated the normalizeCharacter function to use optional chaining for plugin properties, enhancing null safety. - Removed unnecessary return statements to streamline the code flow, ensuring that the function consistently returns the plugin object when valid. * feat: apply di in agent index.ts * feat: enhance error handling and null safety in plugin-di - Updated the `createPlugin` function to improve error handling for providers, actions, and evaluators by logging errors and filtering out undefined values. - Implemented optional chaining in the `buildContentOutputTemplate` function to enhance null safety when accessing examples. - Added a new test suite for normalizing characters, ensuring the integrity of plugins and their properties. * docs: update README.md for Dependency Injection Plugin * fix: correct typo in comment for character normalization in index.ts * feat: add external dependencies for plugin-di - Included "inversify", "reflect-metadata", "zod", and "uuid" as external modules in the tsup configuration to enhance functionality and support for dependency injection and validation. * fix: pnpm-lock.yaml * refactor: remove ExtendedPlugin type and update createPlugin return type - Removed the ExtendedPlugin type definition from types.ts to simplify the type structure. - Updated the createPlugin function in plugin.ts to return a Plugin type instead of ExtendedPlugin, reflecting the changes in the type definitions. * Update packages/plugin-di/src/actions/baseInjectableAction.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update packages/plugin-di/src/actions/baseInjectableAction.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update packages/plugin-di/src/actions/baseInjectableAction.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update package.json * Refactor: fix typo, rename Injactable to Injectable in plugin-di This commit updates the naming convention from `Injactable` to `Injectable` across the plugin-di package, enhancing clarity and consistency. Changes include: - Renaming classes and interfaces in README.md, types.ts, and example files. - Introducing a new `BaseInjectableEvaluator` class to replace the previous `BaseInjactableEvaluator`. - Updating imports and references throughout the codebase to reflect the new naming. These modifications improve the overall readability and maintainability of the code. * chore: update dependencies and fix typos in plugin-di - Removed outdated dependencies from pnpm-lock.yaml. - Updated zod version from ^3.24.1 to 3.23.8 in package.json and pnpm-lock.yaml. - Fixed import typos in samplePlugin.ts and README.md, changing 'sampleEvalutor' to 'sampleEvaluator'. - Added new sampleEvaluator class in sampleEvaluator.ts for improved functionality. - Enhanced test coverage in normalizeCharacter.test.ts to handle edge cases. These changes improve dependency management and code clarity. * fix: improve plugin normalization and export syntax in plugin-di - Updated the export syntax in index.ts for better clarity by removing quotes around "symbols". - Enhanced the normalization process in charactor.ts to log warnings when some plugins fail to normalize, improving error handling and debugging. These changes enhance code readability and robustness in plugin handling. * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: enhance error handling and code clarity in plugin-di - Improved error handling in CreateResourceAction by logging a specific error message when no content is provided. - Refactored plugin factory functions to utilize a new helper function, getInstanceFromContainer, for better readability and maintainability. - Streamlined the normalization process for providers, actions, and evaluators, ensuring consistent error logging and filtering of undefined values. These changes enhance the robustness and clarity of the plugin handling process. * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: resolve coderabbitai * Update packages/plugin-di/src/_examples/sampleAction.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: update pnpm-lock.yaml * fix: pnpm lock * fix: update pnpm-lock and revert wrong modifications in default samplePlugin * chore: change version to 0.1.8+build.1 like others * fix: pnpm-lock * fix: pnpm-lock.yaml * fix: pnpm-lock.yaml * chore: Update version and README for @elizaos/plugin-di * refactor: update access modifiers in BaseInjectableAction and clean up sampleEvaluator imports - Changed private properties to protected in BaseInjectableAction for better inheritance support. - Cleaned up imports in sampleEvaluator by removing unused State import and standardizing string quotes. * refactor: change return type of processMessages callback in BaseInjectableAction to Promise<any | null> - Updated the return type of the callback in the BaseInjectableAction class to allow for more flexible handling of results, returning null instead of void. * chore: update pkg --------- Co-authored-by: Sayo <hi@sayo.wtf> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 224d1a4 commit 9c1e4f5

30 files changed

+1695
-39
lines changed

agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@elizaos/plugin-binance": "workspace:*",
4444
"@elizaos/plugin-avail": "workspace:*",
4545
"@elizaos/plugin-bootstrap": "workspace:*",
46+
"@elizaos/plugin-di": "workspace:*",
4647
"@elizaos/plugin-cosmos": "workspace:*",
4748
"@elizaos/plugin-intiface": "workspace:*",
4849
"@elizaos/plugin-coinbase": "workspace:*",

agent/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import { zgPlugin } from "@elizaos/plugin-0g";
4141

4242
import { bootstrapPlugin } from "@elizaos/plugin-bootstrap";
43+
import { normalizeCharacter } from "@elizaos/plugin-di";
4344
import createGoatPlugin from "@elizaos/plugin-goat";
4445
// import { intifacePlugin } from "@elizaos/plugin-intiface";
4546
import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation";
@@ -1204,6 +1205,9 @@ const startAgents = async () => {
12041205
characters = await loadCharacters(charactersArg);
12051206
}
12061207

1208+
// Normalize characters for injectable plugins
1209+
characters = await Promise.all(characters.map(normalizeCharacter));
1210+
12071211
try {
12081212
for (const character of characters) {
12091213
await startAgent(character, directClient);

packages/plugin-di/.npmignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*
2+
3+
!dist/**
4+
!package.json
5+
!readme.md
6+
!tsup.config.ts

packages/plugin-di/README.md

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# @elizaos/plugin-di - Dependency Injection Plugin for Eliza
2+
3+
This plugin provides a dependency injection system for Eliza plugins.
4+
5+
## What is Dependency Injection?
6+
7+
Dependency Injection is a design pattern that allows you to inject dependencies into a class or function. This pattern is useful for decoupling components and making your code more modular and testable.
8+
9+
## Examples of How to build a Plugin using Dependency Injection
10+
11+
Check the [example](./src/_examples/) folder for a simple example of how to create a plugin using Dependency Injection.
12+
13+
## Decorators for Dependency Injection
14+
15+
This plugin provides a set of decorators that you can use to inject dependencies into your classes or functions.
16+
17+
### From inversify
18+
19+
We use the [inversify](https://inversify.io/) library to provide the dependency injection system.
20+
The following decorators are provided by the [inversify](https://inversify.io/) library.
21+
22+
#### `@injectable`
23+
24+
> Category: Class Decorator
25+
26+
This decorator marks a class as injectable. This means that you can inject this class into other classes using the `@inject` decorator.
27+
28+
```typescript
29+
import { injectable } from "inversify";
30+
31+
@injectable()
32+
class SampleClass {
33+
}
34+
```
35+
36+
Remember to register the class with the container before injecting it into other classes.
37+
38+
```typescript
39+
import { globalContainer } from "@elizaos/plugin-di";
40+
41+
// Register the class with the container as a singleton, this means that the class will be instantiated only once.
42+
globalContainer.bind(SingletonClass).toSelf().inSingletonScope();
43+
// Register the class with the container as a request context, this means that the class will be instantiated for each request(in this case means each Character).
44+
globalContainer.bind(CharactorContextClass).toSelf().inRequestScope();
45+
```
46+
47+
#### `@inject`
48+
49+
> Category: Parameter Decorator
50+
51+
This decorator marks a parameter as an injection target. This means that the parameter will be injected with the appropriate dependency when the class is instantiated.
52+
53+
```typescript
54+
import { injectable, inject } from "inversify";
55+
56+
@injectable()
57+
class SampleClass {
58+
constructor(
59+
// Inject the SampleDependency as a public property of the class.
60+
@inject("SampleDependency") public sampleDependency: SampleDependency
61+
) {}
62+
}
63+
```
64+
65+
### From di plugin
66+
67+
DI plugin provides abstract classes that you can extend to create Injectable actions or evaluators.
68+
And that provides the following decorators to improve the readability of the code.
69+
70+
#### `@property`
71+
72+
> Category: Property Decorator
73+
74+
This decorator is used to define a property in an action content class which will be used to generate the action content object Schema and content description template for LLM object generation.
75+
76+
```typescript
77+
import { z } from 'zod';
78+
import { property } from "@elizaos/plugin-di";
79+
80+
class SampleActionContent {
81+
@property({
82+
description: "Sample property description",
83+
schema: z.string(),
84+
})
85+
sampleProperty: string;
86+
}
87+
```
88+
89+
## Abstract Classes for Injaectable Actions and Evaluators
90+
91+
This plugin provides abstract classes that you can extend to create Injectable actions or evaluators.
92+
93+
### `BaseInjectableAction`
94+
95+
This abstract class simplify the creation of injectable actions.
96+
You don't need to think about the template for content generation, it will be generated automatically based on the properties of the content Class.
97+
What you need to implement is the `execute` method.
98+
99+
```typescript
100+
import { injectable } from "inversify";
101+
import { BaseInjectableAction } from "@elizaos/plugin-di";
102+
103+
class SampleActionContent {
104+
@property({
105+
description: "Sample property description",
106+
schema: z.string(),
107+
})
108+
property1: string;
109+
}
110+
111+
@injectable()
112+
class SampleAction extends BaseInjectableAction<SampleActionContent> {
113+
constructor() {
114+
super({
115+
/** general action constent options */
116+
contentClass: SampleActionContent,
117+
});
118+
}
119+
120+
/**
121+
* It will be called by `handler` function when the action is triggered.
122+
*/
123+
async execute(
124+
content: SampleActionContent | null,
125+
runtime: IAgentRuntime,
126+
message: Memory,
127+
state: State,
128+
callback?: HandlerCallback
129+
): Promise<void> {
130+
// Your action logic here
131+
}
132+
}
133+
```
134+
135+
### `BaseInjectableEvaluator`
136+
137+
This abstract class simplify the creation of injectable evaluators.
138+
139+
Please refer to the [sampleEvaluator](./src/_examples/sampleEvaluator.ts) for an example of how to create an evaluator.

packages/plugin-di/eslint.config.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import eslintGlobalConfig from "../../eslint.config.mjs";
2+
3+
export default [...eslintGlobalConfig];

packages/plugin-di/package.json

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@elizaos/plugin-di",
3+
"version": "0.1.9-alpha.1",
4+
"type": "module",
5+
"main": "dist/index.js",
6+
"module": "dist/index.js",
7+
"types": "dist/index.d.ts",
8+
"exports": {
9+
"./package.json": "./package.json",
10+
".": {
11+
"import": {
12+
"@elizaos/source": "./src/index.ts",
13+
"types": "./dist/index.d.ts",
14+
"default": "./dist/index.js"
15+
}
16+
}
17+
},
18+
"files": [
19+
"dist"
20+
],
21+
"dependencies": {
22+
"@elizaos/core": "workspace:*",
23+
"inversify": "^6.2.1",
24+
"reflect-metadata": "^0.2.2",
25+
"uuid": "11.0.3",
26+
"zod": "3.23.8"
27+
},
28+
"devDependencies": {
29+
"@types/node": "^20.0.0",
30+
"@types/uuid": "10.0.0",
31+
"tsup": "8.3.5",
32+
"vitest": "2.1.4"
33+
},
34+
"scripts": {
35+
"build": "tsup --format esm --dts",
36+
"dev": "tsup --format esm --dts --watch",
37+
"lint": "eslint --fix --cache .",
38+
"test": "vitest run"
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
IAgentRuntime,
3+
Memory,
4+
HandlerCallback,
5+
State,
6+
elizaLogger,
7+
} from "@elizaos/core";
8+
import { z } from "zod";
9+
import { inject, injectable } from "inversify";
10+
import { BaseInjectableAction } from "../actions";
11+
import { ActionOptions } from "../types";
12+
import { property } from "../decorators";
13+
import { globalContainer } from "../di";
14+
import { SampleProvider } from "./sampleProvider";
15+
16+
/**
17+
* The content class for the action
18+
*/
19+
export class CreateResourceContent {
20+
@property({
21+
description: "Name of the resource",
22+
schema: z.string(),
23+
})
24+
name: string;
25+
26+
@property({
27+
description: "Type of resource (document, image, video)",
28+
schema: z.string(),
29+
})
30+
type: string;
31+
32+
@property({
33+
description: "Description of the resource",
34+
schema: z.string(),
35+
})
36+
description: string;
37+
38+
@property({
39+
description: "Array of tags to categorize the resource",
40+
schema: z.array(z.string()),
41+
})
42+
tags: string[];
43+
}
44+
45+
/**
46+
* Options for the CreateResource action
47+
*/
48+
const options: ActionOptions<CreateResourceContent> = {
49+
name: "CREATE_RESOURCE",
50+
similes: [],
51+
description: "Create a new resource with the specified details",
52+
examples: [
53+
[
54+
{
55+
user: "{{user1}}",
56+
content: {
57+
text: "Create a new resource with the name 'Resource1' and type 'TypeA'",
58+
},
59+
},
60+
{
61+
user: "{{agentName}}",
62+
content: {
63+
text: `Resource created successfully:
64+
- Name: Resource1
65+
- Type: TypeA`,
66+
},
67+
},
68+
],
69+
[
70+
{
71+
user: "{{user1}}",
72+
content: {
73+
text: "Create a new resource with the name 'Resource2' and type 'TypeB'",
74+
},
75+
},
76+
{
77+
user: "{{agentName}}",
78+
content: {
79+
text: `Resource created successfully:
80+
- Name: Resource2
81+
- Type: TypeB`,
82+
},
83+
},
84+
],
85+
],
86+
contentClass: CreateResourceContent,
87+
};
88+
89+
/**
90+
* CreateResourceAction
91+
*/
92+
@injectable()
93+
export class CreateResourceAction extends BaseInjectableAction<CreateResourceContent> {
94+
constructor(
95+
@inject(SampleProvider)
96+
private readonly sampleProvider: SampleProvider
97+
) {
98+
super(options);
99+
}
100+
101+
async validate(
102+
runtime: IAgentRuntime,
103+
_message: Memory,
104+
_state?: State
105+
): Promise<boolean> {
106+
return !!runtime.character.settings.secrets?.API_KEY;
107+
}
108+
109+
async execute(
110+
content: CreateResourceContent | null,
111+
runtime: IAgentRuntime,
112+
message: Memory,
113+
state: State,
114+
callback?: HandlerCallback
115+
): Promise<any | null> {
116+
if (!content) {
117+
const error = "No content provided for the action.";
118+
elizaLogger.warn(error);
119+
await callback?.({ text: error }, []);
120+
return;
121+
}
122+
123+
// Call injected provider to do some work
124+
try {
125+
const result = await this.sampleProvider.get(
126+
runtime,
127+
message,
128+
state
129+
);
130+
if (!result) {
131+
elizaLogger.warn("Provider did not return a result.");
132+
} else {
133+
elizaLogger.info("Privder result:", result);
134+
}
135+
// Use result in callback
136+
} catch (error) {
137+
elizaLogger.error("Provider error:", error);
138+
}
139+
140+
// persist relevant data if needed to memory/knowledge
141+
// const memory = {
142+
// type: "resource",
143+
// content: resourceDetails.object,
144+
// timestamp: new Date().toISOString()
145+
// };
146+
147+
// await runtime.storeMemory(memory);
148+
149+
callback?.(
150+
{
151+
text: `Resource created successfully:
152+
- Name: ${content.name}
153+
- Type: ${content.type}
154+
- Description: ${content.description}
155+
- Tags: ${content.tags.join(", ")}
156+
157+
Resource has been stored in memory.`,
158+
},
159+
[]
160+
);
161+
}
162+
}
163+
164+
// Register the action with the global container
165+
globalContainer.bind(CreateResourceAction).toSelf().inRequestScope();

0 commit comments

Comments
 (0)