Skip to content

Commit f9622c0

Browse files
committed
more effect and stemming
1 parent 35de9c4 commit f9622c0

File tree

2 files changed

+102
-75
lines changed

2 files changed

+102
-75
lines changed

site/agents-mcp/src/index.ts

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod";
44
import { createMcpHandler } from "agents/mcp";
55
import { fetchAndBuildIndex, formatResults } from "./utils";
66
import { search } from "@orama/orama";
7+
import { Effect } from "effect";
78

89
// TODO: instrument this server for observability
910
const mcpServer = new McpServer(
@@ -18,8 +19,12 @@ const mcpServer = new McpServer(
1819
);
1920

2021
const inputSchema = {
21-
query: z.string(),
22-
k: z.number().optional().default(10)
22+
query: z
23+
.string()
24+
.describe(
25+
"query string to search for eg. 'agent hibernate', 'schedule tasks'"
26+
),
27+
k: z.number().optional().default(5).describe("number of results to return")
2328
};
2429

2530
mcpServer.registerTool(
@@ -28,37 +33,45 @@ mcpServer.registerTool(
2833
inputSchema
2934
},
3035
async ({ query, k }) => {
31-
try {
36+
const searchEffect = Effect.gen(function* () {
3237
console.log({ query, k });
33-
const docsDb = await fetchAndBuildIndex();
34-
const results = await search(docsDb, {
35-
term: query,
36-
limit: k
37-
});
38-
return {
39-
content: [
40-
{
41-
type: "text",
42-
text: formatResults(results, query, k)
43-
}
44-
]
45-
};
46-
} catch (error) {
47-
console.error(error);
38+
const term = query.trim();
39+
40+
const docsDb = yield* fetchAndBuildIndex;
41+
42+
const result = search(docsDb, { term, limit: k });
43+
const searchResult = yield* result instanceof Promise
44+
? Effect.promise(() => result)
45+
: Effect.succeed(result);
46+
4847
return {
4948
content: [
5049
{
51-
type: "text",
52-
text: `There was an error with the search tool. Please try again later.`
50+
type: "text" as const,
51+
text: formatResults(searchResult, term, k)
5352
}
5453
]
5554
};
56-
}
55+
}).pipe(
56+
Effect.catchAll((error) => {
57+
console.error(error);
58+
return Effect.succeed({
59+
content: [
60+
{
61+
type: "text" as const,
62+
text: `There was an error with the search tool. Please try again later.`
63+
}
64+
]
65+
});
66+
})
67+
);
68+
69+
return await Effect.runPromise(searchEffect);
5770
}
5871
);
5972

6073
export default {
6174
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
62-
return createMcpHandler(mcpServer as any)(request, env, ctx);
75+
return createMcpHandler(mcpServer)(request, env, ctx);
6376
}
6477
};

site/agents-mcp/src/utils.ts

Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ const KV_KEY = "docs-v0";
2020
const DOCS_REPO_API =
2121
"https://api.github.com/repos/cloudflare/agents/git/trees/main?recursive=1";
2222
const TTL_SECONDS = 24 * 60 * 60; // 1 day
23+
const CHUNK_SIZE = 2000;
24+
const MIN_CHARS_PER_CHUNK = 500;
25+
26+
const chunker = await RecursiveChunker.create({
27+
chunkSize: CHUNK_SIZE,
28+
minCharactersPerChunk: MIN_CHARS_PER_CHUNK
29+
});
2330

2431
const fetchWithRetry = (url: string, useAuth = true) =>
2532
Effect.tryPromise({
@@ -57,8 +64,8 @@ const fetchWithRetry = (url: string, useAuth = true) =>
5764
)
5865
);
5966

60-
const fetchDocsFromGitHub = async (): Promise<Document[]> => {
61-
const treeEffect = fetchWithRetry(DOCS_REPO_API).pipe(
67+
const fetchDocsFromGitHub = Effect.gen(function* () {
68+
const treeData = yield* fetchWithRetry(DOCS_REPO_API).pipe(
6269
Effect.flatMap((response) =>
6370
Effect.tryPromise({
6471
try: async () => {
@@ -73,89 +80,90 @@ const fetchDocsFromGitHub = async (): Promise<Document[]> => {
7380
)
7481
);
7582

76-
const treeData = await Effect.runPromise(treeEffect);
77-
7883
const docFiles = treeData.tree.filter(
7984
(item: GitHubTreeItem) =>
8085
item.path.startsWith("docs/") && item.path.endsWith(".md")
8186
);
8287

8388
const docs: Document[] = [];
84-
const chunker = await RecursiveChunker.create({
85-
chunkSize: 800
86-
});
8789

8890
for (const file of docFiles) {
8991
const contentUrl = `https://raw.githubusercontent.com/cloudflare/agents/main/${file.path}`;
9092

91-
const contentEffect = fetchWithRetry(contentUrl).pipe(
93+
const contentResult = yield* fetchWithRetry(contentUrl).pipe(
9294
Effect.flatMap((response) =>
9395
Effect.tryPromise({
9496
try: () => response.text(),
9597
catch: (error) => error as Error
9698
})
97-
)
99+
),
100+
Effect.flatMap((content) => Effect.promise(() => chunker.chunk(content))),
101+
Effect.catchAll((error) => {
102+
console.error(`Failed to fetch/chunk ${file.path}:`, error);
103+
return Effect.succeed([]);
104+
})
98105
);
99106

100-
try {
101-
const content = await Effect.runPromise(contentEffect);
102-
const chunks = await chunker.chunk(content);
103-
104-
for (const chunk of chunks) {
105-
docs.push({
106-
fileName: file.path,
107-
content: chunk.text,
108-
url: contentUrl
109-
});
110-
}
111-
} catch (error) {
112-
console.error(`Failed to fetch/chunk ${file.path}:`, error);
107+
for (const chunk of contentResult) {
108+
docs.push({
109+
fileName: file.path,
110+
content: chunk.text,
111+
url: contentUrl
112+
});
113113
}
114114
}
115115

116116
return docs;
117-
};
118-
119-
const getCachedDocs = async (): Promise<Document[] | null> => {
120-
const cached = await env.DOCS_KV.get(KV_KEY, "json");
121-
122-
if (!cached) {
123-
return null;
124-
}
117+
});
125118

126-
return cached as Document[];
127-
};
119+
const getCachedDocs = Effect.tryPromise({
120+
try: () => env.DOCS_KV.get(KV_KEY, "json") as Promise<Document[] | null>,
121+
catch: (error) => error as Error
122+
});
128123

129-
const cacheDocs = async (docs: Document[]): Promise<void> => {
130-
await env.DOCS_KV.put(KV_KEY, JSON.stringify(docs), {
131-
expirationTtl: TTL_SECONDS
124+
const cacheDocs = (docs: Document[]) =>
125+
Effect.tryPromise({
126+
try: () =>
127+
env.DOCS_KV.put(KV_KEY, JSON.stringify(docs), {
128+
expirationTtl: TTL_SECONDS
129+
}),
130+
catch: (error) => error as Error
132131
});
133-
};
134132

135-
export const fetchAndBuildIndex = async () => {
136-
let docs = await getCachedDocs();
133+
export const fetchAndBuildIndex = Effect.gen(function* () {
134+
const cached = yield* getCachedDocs;
135+
136+
let docs: Document[];
137137

138-
if (!docs) {
139-
// If not cached, fetch from GitHub, chunk, and cache to KV
140-
docs = await fetchDocsFromGitHub();
141-
await cacheDocs(docs);
138+
if (!cached) {
139+
docs = yield* fetchDocsFromGitHub;
140+
yield* cacheDocs(docs);
141+
} else {
142+
docs = cached;
142143
}
143144

144-
// Build the search index from docs
145-
const docsDb = create({
146-
schema: {
147-
fileName: "string",
148-
content: "string",
149-
url: "string"
150-
} as const
151-
});
145+
const docsDb = yield* Effect.sync(() =>
146+
create({
147+
schema: {
148+
fileName: "string",
149+
content: "string",
150+
url: "string"
151+
} as const,
152+
components: {
153+
tokenizer: {
154+
stemming: true,
155+
language: "english"
156+
}
157+
}
158+
})
159+
);
152160

153161
for (const doc of docs) {
154-
await insert(docsDb, doc);
162+
yield* Effect.sync(() => insert(docsDb, doc));
155163
}
156164

157165
return docsDb;
158-
};
166+
});
159167

160168
export const formatResults = (
161169
results: Awaited<ReturnType<typeof search>>,
@@ -167,7 +175,13 @@ export const formatResults = (
167175

168176
let output = `**Search Results**\n\n`;
169177
output += `Found ${hitCount} result${hitCount !== 1 ? "s" : ""} for "${query}" (${elapsed})\n\n`;
170-
output += `Showing top ${k} result${k !== 1 ? "s" : ""}:\n\n`;
178+
179+
if (hitCount === 0) {
180+
output += `No results found. Try using different keywords or modify the spelling.`;
181+
return output;
182+
}
183+
184+
output += `Showing top ${Math.min(k, hitCount)} result${Math.min(k, hitCount) !== 1 ? "s" : ""}:\n\n`;
171185
output += `---\n\n`;
172186

173187
for (const hit of results.hits) {

0 commit comments

Comments
 (0)