Skip to content

Commit 0671848

Browse files
committed
Add knowledge tool to the assistant
1 parent 61e2662 commit 0671848

File tree

4 files changed

+150
-47
lines changed

4 files changed

+150
-47
lines changed

src/aggregator/knowledge-aggregator.ts

+6-44
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,25 @@
1-
import fs from "fs";
2-
import path from "path";
3-
import matter from "gray-matter";
4-
5-
import { DynamicKnowledge } from "@prisma/client";
6-
import { prisma } from "../prisma";
71
import { WebhookAlertDto } from "../checkly/alertDTO";
82
import { CheckContext, ContextKey } from "./ContextAggregator";
3+
import { getAllDocuments, KnowledgeDocument, } from "../knowledge-base/knowledgeBase";
94

10-
const transformDocument = (fileContent: string, checkId: string): CheckContext => {
11-
const { data } = matter(fileContent);
12-
5+
const transformDocument = (document: KnowledgeDocument, checkId: string): CheckContext => {
136
return {
147
checkId,
15-
value: fileContent,
8+
value: document.fullContent,
169
source: 'knowledge',
1710
key: ContextKey.Knowledge.replace(
1811
"$documentSlug",
19-
data.slug
12+
document.slug
2013
),
21-
analysis: data.summary,
14+
analysis: document.summary,
2215
} as CheckContext;
2316
}
2417

25-
const loadKnowledgeDocuments = async (directory: string): Promise<string[]> => {
26-
const collectMarkdownFiles = (dir: string): string[] => {
27-
const entries = fs.readdirSync(dir, { withFileTypes: true });
28-
const files: string[] = [];
29-
30-
for (const entry of entries) {
31-
const fullPath = path.join(dir, entry.name);
32-
if (entry.isDirectory()) {
33-
// Recursively collect files from subdirectories
34-
files.push(...collectMarkdownFiles(fullPath));
35-
} else if (entry.isFile() && fullPath.endsWith(".md")) {
36-
// Add Markdown files
37-
files.push(fullPath);
38-
}
39-
}
40-
41-
return files;
42-
};
43-
44-
const markdownFiles = collectMarkdownFiles(directory);
45-
46-
return markdownFiles.map((filePath) => fs.readFileSync(filePath, "utf-8"));
47-
};
48-
49-
const loadDynamicKnowledge = async (): Promise<string[]> => {
50-
const documents = await prisma.dynamicKnowledge.findMany({}) as DynamicKnowledge[]
51-
52-
return documents.map((doc) => doc.content);
53-
};
54-
55-
5618
export const knowledgeAggregator = {
5719
name: "Knowledge",
5820
fetchContext: async (alert: WebhookAlertDto): Promise<CheckContext[]> => {
5921
console.log('Aggregating Knowledge Context...');
60-
const documents = [...await loadKnowledgeDocuments("./data/knowledge"), ...await loadDynamicKnowledge()];
22+
const documents = await getAllDocuments()
6123

6224
return documents.map((doc) => transformDocument(doc, alert.CHECK_ID));
6325
},

src/knowledge-base/knowledgeBase.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import matter from "gray-matter";
4+
5+
import { DynamicKnowledge } from "@prisma/client";
6+
import { prisma } from "../prisma";
7+
8+
// This path assumes that the working directory is the root directory of the project
9+
const STATIC_KNOWLEDGE_DIRECTORY = "./data/knowledge";
10+
11+
export type KnowledgeDocument = {
12+
content: string;
13+
fullContent: string;
14+
title: string
15+
slug: string
16+
summary: string
17+
created: Date
18+
updated: Date
19+
}
20+
21+
const parseDocument = (rawDocument: string): KnowledgeDocument => {
22+
const parsed = matter(rawDocument);
23+
24+
return {
25+
slug: parsed.data.slug,
26+
summary: parsed.data.summary,
27+
created: new Date(parsed.data.created),
28+
updated: new Date(parsed.data.updated),
29+
fullContent: rawDocument,
30+
content: parsed.content,
31+
title: parsed.data.title
32+
}
33+
}
34+
35+
export const loadKnowledgeDocuments = async (directory: string = STATIC_KNOWLEDGE_DIRECTORY): Promise<KnowledgeDocument[]> => {
36+
const collectMarkdownFiles = (dir: string): string[] => {
37+
const entries = fs.readdirSync(dir, { withFileTypes: true });
38+
const files: string[] = [];
39+
40+
for (const entry of entries) {
41+
const fullPath = path.join(dir, entry.name);
42+
if (entry.isDirectory()) {
43+
// Recursively collect files from subdirectories
44+
files.push(...collectMarkdownFiles(fullPath));
45+
} else if (entry.isFile() && fullPath.endsWith(".md")) {
46+
// Add Markdown files
47+
files.push(fullPath);
48+
}
49+
}
50+
51+
return files;
52+
};
53+
54+
const markdownFiles = collectMarkdownFiles(directory);
55+
56+
return markdownFiles.map((filePath) => parseDocument(fs.readFileSync(filePath, "utf-8")));
57+
};
58+
59+
export const loadDynamicKnowledge = async (): Promise<KnowledgeDocument[]> => {
60+
const documents = await prisma.dynamicKnowledge.findMany({}) as DynamicKnowledge[]
61+
62+
return documents.map((doc) => parseDocument(doc.content));
63+
};
64+
65+
export const getAllDocuments = async (): Promise<KnowledgeDocument[]> => {
66+
const staticKnowledge = await loadKnowledgeDocuments();
67+
const dynamicKnowledge = await loadDynamicKnowledge();
68+
69+
return [...staticKnowledge, ...dynamicKnowledge];
70+
}

src/sre-assistant/SreAssistant.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ChecklyTool } from "./tools/ChecklyTool";
66
import { GitHubTool } from "./tools/GitHubTool";
77
import { prisma } from "../prisma";
88
import { slackFormatInstructions } from "../slackbot/utils";
9+
import { KnowledgeTool } from "./tools/KnowledgeTool";
910

1011
export class SreAssistant extends BaseAssistant {
1112
alertId: string | undefined;
@@ -62,7 +63,7 @@ CONSTITUTION:
6263
7. Make active use of the tools (multiple times if needed) to get a holistic view of the situation
6364
8. Generate super short, concise and insightful messages. Users are experts, skip the fluff, no yapping.
6465
9. Context-Driven Analysis: Load the check to understand the failure context and its potential root cause. Cross-reference the alert with logs, metrics, and change histories to identify anomalies or patterns. Please remember that releases (changes) are the most common reason for alerting events, focus on analysing recent changes.
65-
10. Refer to the the knowledge context to build a better understanding of the product, systems and the organisation you are working for. Assume that the users have good knowledge of the systems, and do not proactively provide basic information unless explicitly asked.
66+
10. Refer to the the knowledge base to build a better understanding of the terminology, systems and the organisation you are working for. Assume that the users have good knowledge of the company, and do not proactively provide basic information unless explicitly asked.
6667
6768
INTERACTION CONTEXT:
6869
Username: ${this.interactionContext["username"]}
@@ -76,11 +77,11 @@ ${alertSummary.length > 0 ? `Alert Summary:\n${alertSummary}` : ""}`;
7677

7778
protected async getTools(): Promise<Tool[]> {
7879
if (!this.alertId) {
79-
return [new ChecklyTool(this), new GitHubTool(this)];
80+
return [new ChecklyTool(this), new GitHubTool(this), new KnowledgeTool(this)];
8081
}
8182

8283
const searchContextTool = new SearchContextTool(this);
8384
await searchContextTool.init();
84-
return [searchContextTool, new ChecklyTool(this), new GitHubTool(this)];
85+
return [searchContextTool, new ChecklyTool(this), new GitHubTool(this), new KnowledgeTool(this)];
8586
}
8687
}
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { z } from "zod";
2+
import { createToolOutput, createToolParameters, Tool } from "../../ai/Tool";
3+
import { SreAssistant } from "../SreAssistant";
4+
import { getAllDocuments } from "../../knowledge-base/knowledgeBase";
5+
6+
const parameters = createToolParameters(
7+
z.object({
8+
action: z
9+
.enum([
10+
"listDocuments",
11+
"getOneDocument",
12+
])
13+
.describe("The action to perform on the Knowledge Base"),
14+
documentSlug: z
15+
.string()
16+
.describe(
17+
"The slug of the Document to get information about. Omit this field for the 'listDocuments' action. Required for the 'getOneDocument'"
18+
)
19+
.optional(),
20+
})
21+
);
22+
23+
const outputSchema = createToolOutput(
24+
z.string().describe("The response from the Knowledge Base")
25+
);
26+
27+
export class KnowledgeTool extends Tool<
28+
typeof parameters,
29+
typeof outputSchema,
30+
SreAssistant
31+
> {
32+
static parameters = parameters;
33+
static outputSchema = outputSchema;
34+
35+
constructor(agent: SreAssistant) {
36+
super({
37+
name: "KnowledgeBase",
38+
description:
39+
"Interact with the Knowledge Base to retrieve relevant context about the organisation structure, projects and terminology.",
40+
parameters,
41+
agent,
42+
});
43+
}
44+
45+
async execute(input: z.infer<typeof parameters>) {
46+
if (input.action === "listDocuments") {
47+
const documents = await getAllDocuments();
48+
49+
return JSON.stringify(documents.map((doc) => ({
50+
slug: doc.slug,
51+
title: doc.title,
52+
summary: doc.summary,
53+
})));
54+
} else if (input.action === "getOneDocument") {
55+
if (!input.documentSlug) {
56+
return "Document slug is required";
57+
}
58+
59+
const document = await getAllDocuments().then(docs => docs.find(doc => doc.slug === input.documentSlug));
60+
61+
if (!document) {
62+
return `Document for slug: ${input.documentSlug} not found`;
63+
}
64+
65+
return document.fullContent;
66+
}
67+
68+
return "Invalid action";
69+
}
70+
}

0 commit comments

Comments
 (0)