Skip to content

Commit 06033c1

Browse files
committed
add gitbook plugin/fix double post
1 parent 94d374a commit 06033c1

File tree

8 files changed

+258
-25
lines changed

8 files changed

+258
-25
lines changed

packages/plugin-bootstrap/src/actions/continue.ts

+55-3
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,62 @@ export const continueAction: Action = {
5858
similes: ["ELABORATE", "KEEP_TALKING"],
5959
description:
6060
"ONLY use this action when the message necessitates a follow up. Do not use this action when the conversation is finished or the user does not wish to speak (use IGNORE instead). If the last message action was CONTINUE, and the user has not responded. Use sparingly.",
61+
// validate: async (runtime: IAgentRuntime, message: Memory) => {
62+
// const recentMessagesData = await runtime.messageManager.getMemories({
63+
// roomId: message.roomId,
64+
// count: 10,
65+
// unique: false,
66+
// });
67+
68+
69+
// const agentMessages = recentMessagesData.filter(
70+
// (m: { userId: any }) => m.userId === runtime.agentId
71+
// );
72+
73+
// // check if the last messages were all continues=
74+
// if (agentMessages) {
75+
// const lastMessages = agentMessages.slice(0, maxContinuesInARow);
76+
// if (lastMessages.length >= maxContinuesInARow) {
77+
// const allContinues = lastMessages.every(
78+
// (m: { content: any }) =>
79+
// (m.content as Content).action === "CONTINUE"
80+
// );
81+
// if (allContinues) {
82+
// return false;
83+
// }
84+
// }
85+
// }
86+
87+
// return true;
88+
// },
6189
validate: async (runtime: IAgentRuntime, message: Memory) => {
6290
const recentMessagesData = await runtime.messageManager.getMemories({
6391
roomId: message.roomId,
6492
count: 10,
6593
unique: false,
6694
});
95+
96+
// First check if we have a user response after our last CONTINUE
97+
const sortedMessages = recentMessagesData.sort((a, b) =>
98+
(b.createdAt || 0) - (a.createdAt || 0)
99+
);
100+
101+
const lastUserMessage = sortedMessages.find(m => m.userId !== runtime.agentId);
102+
const lastAgentMessage = sortedMessages.find(m => m.userId === runtime.agentId);
103+
104+
// If the most recent message is from a user and came after our last CONTINUE,
105+
// we shouldn't continue again
106+
if (lastUserMessage && lastAgentMessage &&
107+
lastUserMessage.createdAt > lastAgentMessage.createdAt &&
108+
(lastAgentMessage.content as Content).action === 'CONTINUE') {
109+
return false;
110+
}
111+
112+
// Validation for consecutive CONTINUEs
67113
const agentMessages = recentMessagesData.filter(
68114
(m: { userId: any }) => m.userId === runtime.agentId
69115
);
70116

71-
// check if the last messages were all continues=
72117
if (agentMessages) {
73118
const lastMessages = agentMessages.slice(0, maxContinuesInARow);
74119
if (lastMessages.length >= maxContinuesInARow) {
@@ -91,6 +136,13 @@ export const continueAction: Action = {
91136
options: any,
92137
callback: HandlerCallback
93138
) => {
139+
// First check if validation passed
140+
const isValid = await continueAction.validate(runtime, message);
141+
if (!isValid) {
142+
elizaLogger.info('[CONTINUE] Handler aborted - Failed validation/Already responded');
143+
return;
144+
}
145+
94146
if (
95147
message.content.text.endsWith("?") ||
96148
message.content.text.endsWith("!")
@@ -155,13 +207,13 @@ export const continueAction: Action = {
155207
.filter((m: { userId: any }) => m.userId === runtime.agentId)
156208
.slice(0, maxContinuesInARow + 1)
157209
.some((m: { content: any }) => m.content === message.content);
158-
159210
if (messageExists) {
160211
return;
161212
}
162213

163214
await callback(response);
164215

216+
165217
// if the action is CONTINUE, check if we are over maxContinuesInARow
166218
if (response.action === "CONTINUE") {
167219
const agentMessages = state.recentMessagesData
@@ -598,4 +650,4 @@ export const continueAction: Action = {
598650
},
599651
],
600652
] as ActionExample[][],
601-
} as Action;
653+
} as Action;

packages/plugin-gitbook/package.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@ai16z/plugin-gitbook",
3+
"version": "0.1.0",
4+
"main": "dist/index.js",
5+
"type": "module",
6+
"types": "dist/index.d.ts",
7+
"exports": {
8+
".": {
9+
"import": "./dist/index.js",
10+
"types": "./dist/index.d.ts"
11+
}
12+
},
13+
"files": [
14+
"dist"
15+
],
16+
"dependencies": {
17+
"@ai16z/eliza": "workspace:*",
18+
"tsup": "8.3.5"
19+
},
20+
"scripts": {
21+
"build": "tsup --format esm --dts",
22+
"dev": "tsup --format esm --dts --watch",
23+
"test": "vitest"
24+
}
25+
}

packages/plugin-gitbook/src/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Plugin } from "@ai16z/eliza";
2+
import { gitbookProvider } from "./providers/gitbook";
3+
4+
export const gitbookPlugin: Plugin = {
5+
name: "GitBook Documentation",
6+
description: "Plugin for querying GitBook documentation",
7+
actions: [],
8+
providers: [gitbookProvider],
9+
evaluators: []
10+
};
11+
12+
export default gitbookPlugin;
13+
14+
export * from './types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Provider, IAgentRuntime, Memory, State, elizaLogger } from "@ai16z/eliza";
2+
import { GitBookResponse, GitBookClientConfig } from '../types';
3+
4+
function cleanText(text: string): string {
5+
const cleaned = text
6+
.replace(/<@!?\d+>/g, '') // Discord mentions
7+
.replace(/<#\d+>/g, '') // Discord channels
8+
.replace(/<@&\d+>/g, '') // Discord roles
9+
.replace(/(?:^|\s)@[\w_]+/g, '') // Platform mentions
10+
.trim();
11+
12+
return cleaned;
13+
}
14+
15+
async function validateQuery(runtime: IAgentRuntime, text: string): Promise<boolean> {
16+
// Default general queries - everything else comes from config
17+
let keywords = {
18+
generalQueries: [
19+
'how', 'what', 'where', 'explain', 'show', 'tell',
20+
'can', 'does', 'is', 'are', 'will', 'why',
21+
'benefits', 'features', 'cost', 'price',
22+
'use', 'using', 'work', 'access', 'get'
23+
]
24+
};
25+
26+
try {
27+
const gitbookConfig = runtime.character.clientConfig?.gitbook as GitBookClientConfig;
28+
29+
// Get project terms and document triggers from config
30+
const projectTerms = gitbookConfig?.keywords?.projectTerms || [];
31+
const documentTriggers = gitbookConfig?.documentTriggers || [];
32+
33+
// Merge any additional general queries from config
34+
if (gitbookConfig?.keywords?.generalQueries) {
35+
keywords.generalQueries = [
36+
...keywords.generalQueries,
37+
...gitbookConfig.keywords.generalQueries
38+
];
39+
}
40+
41+
const containsAnyWord = (text: string, words: string[] = []) => {
42+
return words.length === 0 || words.some(word => {
43+
if (word.includes(' ')) {
44+
return text.includes(word.toLowerCase());
45+
}
46+
const regex = new RegExp(`\\b${word}\\b`, 'i');
47+
return regex.test(text);
48+
});
49+
};
50+
51+
const hasProjectTerm = containsAnyWord(text, projectTerms);
52+
const hasDocTrigger = containsAnyWord(text, documentTriggers);
53+
const hasGeneralQuery = containsAnyWord(text, keywords.generalQueries);
54+
55+
const isValid = hasProjectTerm || hasDocTrigger || hasGeneralQuery;
56+
57+
elizaLogger.info(`✅ Is GitBook Validation Result: ${isValid}`)
58+
return isValid;
59+
60+
} catch (error) {
61+
elizaLogger.warn(`❌ Error in GitBook validation:\n${error}`)
62+
return false;
63+
}
64+
}
65+
66+
export const gitbookProvider: Provider = {
67+
get: async (runtime: IAgentRuntime, message: Memory, _state?: State): Promise<string> => {
68+
try {
69+
const spaceId = runtime.getSetting("GITBOOK_SPACE_ID");
70+
if (!spaceId) {
71+
elizaLogger.error('⚠️ GitBook Space ID not configured')
72+
return "";
73+
}
74+
75+
const text = message.content.text.toLowerCase().trim();
76+
const isValidQuery = await validateQuery(runtime, text);
77+
78+
if (!isValidQuery) {
79+
elizaLogger.info('⚠️ GitBook Query validation failed')
80+
return "";
81+
}
82+
83+
const cleanedQuery = cleanText(message.content.text);
84+
85+
const response = await fetch(
86+
`https://api.gitbook.com/v1/spaces/${spaceId}/search/ask`,
87+
{
88+
method: "POST",
89+
headers: {
90+
"Content-Type": "application/json"
91+
},
92+
body: JSON.stringify({
93+
query: cleanedQuery,
94+
variables: {}
95+
})
96+
}
97+
);
98+
99+
if (!response.ok) {
100+
console.error('❌ GitBook API error:', response.status);
101+
return "";
102+
}
103+
104+
const result: GitBookResponse = await response.json();
105+
106+
return result.answer?.text || "";
107+
} catch (error) {
108+
console.error("❌ Error in GitBook provider:", error);
109+
return "";
110+
}
111+
}
112+
};

packages/plugin-gitbook/src/types.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// GitBook API response type
2+
export interface GitBookResponse {
3+
answer?: {
4+
text: string;
5+
};
6+
error?: string;
7+
}
8+
9+
// Client configuration in character.json (all optional)
10+
export interface GitBookKeywords {
11+
projectTerms?: string[];
12+
generalQueries?: string[];
13+
}
14+
15+
export interface GitBookClientConfig {
16+
keywords?: GitBookKeywords;
17+
documentTriggers?: string[];
18+
}

packages/plugin-gitbook/tsconfig.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../core/tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": ".",
6+
"types": [
7+
"node"
8+
]
9+
},
10+
"include": [
11+
"src"
12+
]
13+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "tsup";
2+
3+
export default defineConfig({
4+
entry: ["src/index.ts"],
5+
outDir: "dist",
6+
sourcemap: true,
7+
clean: true,
8+
format: ["esm"],
9+
external: [
10+
"@ai16z/eliza"
11+
],
12+
});

pnpm-lock.yaml

+9-22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)