Skip to content

Commit 0c3e28f

Browse files
/ai/spell/search (toolshed) (#277)
* Stub search functionality * Stubbed search implementation * Implement three basic strategy ideas for search * Extract strategies and refactor logging * Extract LLM library functions * Factor out all redis code into lib * Format pass * Introduce Logger interface * Improve docstrings * Add types for redis * One more format pass * Default cors() settings clipper can reach blobby * Fix lint errors * Reverse refactor, use hono/client Mostly taming errors unsure if anything works * Restore state of world * Fix exported router types * Get all effects working again with hono/client * Format pass * Fix lint * adding a shared llm client wrapper that can be imported and used (#280) * Fix llm call * Fixing errors so I can run the tests * Basic smoke tests for spell endpoints * Feels like this shouldn't work but it does * Comment out tests Need to work out how to handle LLM in CI * Add explaination --------- Co-authored-by: Jake Dahn <jake@common.tools>
1 parent 02ab463 commit 0c3e28f

29 files changed

+1098
-269
lines changed

typescript/packages/toolshed/deno.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"tasks": {
3-
"dev": "deno run -A --watch index.ts",
3+
"dev": "deno run -A --watch --env-file=.env index.ts",
44
"test": "deno test -A --env-file=.env.test",
55
"build-lookslike": "deno run -A scripts/build-lookslike.ts"
66
},
@@ -18,7 +18,7 @@
1818
"rules": {
1919
"tags": ["recommended"],
2020
"include": ["ban-untagged-todo"],
21-
"exclude": ["no-unused-vars"]
21+
"exclude": ["no-unused-vars", "no-explicit-any"]
2222
}
2323
},
2424
"nodeModulesDir": "auto",
@@ -55,6 +55,7 @@
5555
"pino-pretty": "npm:pino-pretty@^13.0.0",
5656
"redis": "npm:redis@^4.7.0",
5757
"stoker": "npm:stoker@^1.4.2",
58-
"zod": "npm:zod@^3.24.1"
58+
"zod": "npm:zod@^3.24.1",
59+
"mistreevous": "npm:mistreevous@4.2.0"
5960
}
6061
}

typescript/packages/toolshed/deno.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { State } from "mistreevous";
2+
import { Logger, PrefixedLogger } from "./prefixed-logger.ts";
3+
4+
// Handles logging and instrumentation
5+
export abstract class BaseAgent {
6+
protected logger: PrefixedLogger;
7+
protected agentName: string;
8+
protected stepDurations: Record<string, number> = {};
9+
protected logs: string[] = [];
10+
protected startTime: number = 0;
11+
12+
constructor(logger: Logger, agentName: string) {
13+
this.agentName = agentName;
14+
this.logger = new PrefixedLogger(logger, agentName);
15+
this.startTime = Date.now();
16+
}
17+
18+
protected async measureStep(
19+
stepName: string,
20+
fn: () => Promise<State>,
21+
): Promise<State> {
22+
const start = Date.now();
23+
const result = await fn();
24+
this.stepDurations[stepName] = Date.now() - start;
25+
this.logger.info(
26+
`${this.agentName}: ${stepName} took ${this.stepDurations[stepName]}ms`,
27+
);
28+
return result;
29+
}
30+
31+
protected getMetadata() {
32+
const totalDuration = Date.now() - this.startTime;
33+
return {
34+
totalDuration,
35+
stepDurations: this.stepDurations,
36+
logs: this.logs,
37+
};
38+
}
39+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { AppType } from "@/app.ts";
2+
import { hc } from "hono/client";
3+
4+
// NOTE(jake): Ideally this would be exposed via the hono client, but I wasn't
5+
// able to get it all wired up. Importing the route definition is fine for now.
6+
import type { GetModelsRouteQueryParams } from "@/routes/ai/llm/llm.routes.ts";
7+
8+
const client = hc<AppType>("http://localhost:8000/");
9+
10+
export async function listAvailableModels({
11+
capability,
12+
task,
13+
search,
14+
}: GetModelsRouteQueryParams) {
15+
const res = await client.api.ai.llm.models.$get({
16+
query: {
17+
search,
18+
capability,
19+
task,
20+
},
21+
});
22+
return res.json();
23+
}
24+
25+
export async function generateText(
26+
query: Parameters<typeof client.api.ai.llm.$post>[0]["json"],
27+
): Promise<string> {
28+
const res = await client.api.ai.llm.$post({ json: query });
29+
const data = await res.json();
30+
31+
if ("error" in data) {
32+
throw new Error(data.error);
33+
}
34+
35+
if ("type" in data && data.type === "json") {
36+
return data.body.content;
37+
}
38+
39+
if ("content" in data) {
40+
// bf: this is actually the case that runs, even if the types disagree
41+
// no idea why
42+
return (data as any).content;
43+
}
44+
45+
throw new Error("Unexpected response from LLM server");
46+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export interface Logger {
2+
// deno-lint-ignore no-explicit-any
3+
info(...args: any[]): void;
4+
// deno-lint-ignore no-explicit-any
5+
error(...args: any[]): void;
6+
// deno-lint-ignore no-explicit-any
7+
warn(...args: any[]): void;
8+
// deno-lint-ignore no-explicit-any
9+
debug(...args: any[]): void;
10+
}
11+
12+
export class PrefixedLogger implements Logger {
13+
private logger: Logger;
14+
private prefix: string;
15+
private logMessages: string[] = [];
16+
17+
constructor(logger: Logger = console, prefix: string) {
18+
this.logger = logger;
19+
this.prefix = prefix;
20+
this.info = this.info.bind(this);
21+
this.error = this.error.bind(this);
22+
this.warn = this.warn.bind(this);
23+
this.debug = this.debug.bind(this);
24+
}
25+
26+
// deno-lint-ignore no-explicit-any
27+
info(...args: any[]) {
28+
const message = [`[${this.prefix}]`, ...args].join(" ");
29+
this.logMessages.push(message);
30+
this.logger.info(message);
31+
}
32+
33+
// deno-lint-ignore no-explicit-any
34+
error(...args: any[]) {
35+
const message = [`[${this.prefix}]`, ...args].join(" ");
36+
this.logMessages.push(message);
37+
this.logger.error(message);
38+
}
39+
40+
// deno-lint-ignore no-explicit-any
41+
warn(...args: any[]) {
42+
const message = [`[${this.prefix}]`, ...args].join(" ");
43+
this.logMessages.push(message);
44+
this.logger.warn(message);
45+
}
46+
47+
// deno-lint-ignore no-explicit-any
48+
debug(...args: any[]) {
49+
const message = [`[${this.prefix}]`, ...args].join(" ");
50+
this.logMessages.push(message);
51+
this.logger.debug(message);
52+
}
53+
54+
getLogs(): string[] {
55+
return this.logMessages;
56+
}
57+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export async function handleResponse<T>(response: Response): Promise<T> {
2+
const data = await response.json();
3+
4+
if ("error" in data) {
5+
throw new Error(data.error);
6+
}
7+
8+
if (data.type === "json") {
9+
return data.body;
10+
}
11+
12+
if (data instanceof Response) {
13+
throw new Error(data.statusText);
14+
}
15+
16+
return null as never;
17+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Schema, Validator } from "jsonschema";
2+
3+
export function checkSchemaMatch(
4+
data: Record<string, unknown>,
5+
schema: Schema,
6+
): boolean {
7+
const validator = new Validator();
8+
9+
const jsonSchema: unknown = {
10+
type: "object",
11+
properties: Object.keys(schema).reduce(
12+
(acc: Record<string, unknown>, key) => {
13+
const schemaValue = schema[key as keyof Schema];
14+
acc[key] = { type: (schemaValue as any)?.type || typeof schemaValue };
15+
return acc;
16+
},
17+
{},
18+
),
19+
required: Object.keys(schema),
20+
additionalProperties: true,
21+
};
22+
23+
const rootResult = validator.validate(data, jsonSchema as Schema);
24+
if (rootResult.valid) {
25+
return true;
26+
}
27+
28+
function checkSubtrees(obj: unknown): boolean {
29+
if (typeof obj !== "object" || obj === null) {
30+
return false;
31+
}
32+
33+
if (Array.isArray(obj)) {
34+
return obj.some((item) => checkSubtrees(item));
35+
}
36+
37+
const result = validator.validate(obj, jsonSchema as Schema);
38+
if (result.valid) {
39+
return true;
40+
}
41+
42+
return Object.values(obj).some((value) => checkSubtrees(value));
43+
}
44+
45+
return checkSubtrees(data);
46+
}

typescript/packages/toolshed/lib/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { OpenAPIHono, RouteConfig, RouteHandler } from "@hono/zod-openapi";
2-
import type { PinoLogger } from "hono-pino";
2+
import type { Logger } from "pino";
33
import type { RedisClientType } from "redis";
44

55
export interface AppBindings {
66
Variables: {
7-
logger: PinoLogger;
7+
logger: Logger;
88
blobbyRedis: RedisClientType;
99
};
1010
}

typescript/packages/toolshed/routes/ai/llm/cache.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export async function loadItem(key: string): Promise<CacheItem | null> {
2525
const cacheData = await Deno.readTextFile(filePath);
2626
console.log(
2727
`${timestamp()} ${colors.green}📦 Cache loaded:${colors.reset} ${
28-
filePath.slice(-12)
28+
filePath.slice(
29+
-12,
30+
)
2931
}`,
3032
);
3133
return JSON.parse(cacheData);

0 commit comments

Comments
 (0)