Readme

Allows for automatic generation of Hono API compatible with GPTs. Endpoints' inputs and outputs need to be specified via types from which the Open API spec is generated automatically and available via /gpt/schema endpoint.

⚠️ Breaking changes introduced in v23 & 24:

  • nothingToJson doesn't take the generic TResponse anymore. The type is inferred from the endpoint definition. The endpoint definition doesn't need the requestSchema, requestDesc and responseDesc defined anymore. The descriptions are inferred from the schema description.
  • jsonToJson doesn't take the generic TRequest and TResponse anymore. Types are inferred from the endpoint definition. The endpoint definition doesn't need the requestDesc and responseDesc defined anymore. The descriptions are inferred from the schema description.

Usage example:

import { GptApi } from "https://esm.town/v/xkonti/gptApiFramework"; import { z } from "npm:zod"; /** * COMMON TYPES */ const ResponseCommandSchema = z.object({ feedback: z.string().describe("Feedback regarding submitted action"), command: z.string().describe("The command for the Mediator AI to follow strictly"), data: z.string().optional().describe("Additional data related to the given command"), }).describe("Contains feedback and further instructions to follow"); /** * INITIALIZE API */ const api = new GptApi({ url: "https://xkonti-planoverseerai.web.val.run", title: "Overseer AI API", description: "The API for interacting with the Overseer AI", version: "1.0.0", policyGetter: async () => { const { markdownToPrettyPage } = await import("https://esm.town/v/xkonti/markdownToHtmlPage?v=5"); return await markdownToPrettyPage("# Privacy Policy\n\n## Section 1..."); }, }); /** * REQUIREMENTS GATHERING ENDPOINTS */ api.nothingToJson({ verb: "POST", path: "/newproblem", operationId: "new-problem", desc: "Endpoint for informing Overseer AI about a new problem presented by the User", responseSchema: ResponseCommandSchema, responseDesc: "Instruction on how to proceed with the new problem", }, async (ctx) => { return { feedback: "User input downloaded. Problem analysis is required.", command: await getPrompt("analyze-problem"), data: "", }; }); export default api.serve();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { EndpointDefinition, getOpenApiSpec } from "https://esm.town/v/xkonti/gptApiSchemaBuilder";
import { HttpVerb } from "https://esm.town/v/xkonti/httpUtils";
import { Hono } from "npm:hono@3";
import { z } from "npm:zod";
export interface ApiInfo {
url: string;
title: string;
description: string;
version: string;
}
export class GptApi {
app = new Hono();
info: ApiInfo;
endpoints: EndpointDefinition[] = [];
constructor(info: ApiInfo) {
this.info = info;
this.app.get("/gpt/schema", async (ctx) => {
const spec = getOpenApiSpec(
this.info.url,
this.info.title,
this.info.description,
this.info.version,
this.endpoints,
);
return ctx.text(JSON.stringify(spec, null, 2));
});
}
/**
* Register endpoint that ignores all inputs and returns a specific JSON response.
*/
nothingToJson<TResponse>(
endpointDef: EndpointDefinition,
handler: (ctx) => Promise<TResponse>,
) {
endpointDef.requestDesc = null;
endpointDef.requestSchema = null;
const handlerWrapper = async (ctx) => {
const response = await handler(ctx);
return ctx.json(response);
};
// TODO: Verify response stuff is in place
this.registerHandler(endpointDef, handlerWrapper);
}
/**
* Register endpoint that accepts a JSON DTO and returns a specific JSON response.
*/
jsonToJson<TRequest, TResponse>(
endpointDef: EndpointDefinition,
handler: (ctx, dto: TRequest) => Promise<TResponse>,
) {
// TODO: Verify request and response is in place
const handlerWrapper = async (ctx) => {
const data = await ctx.req.json() as TRequest;
// TODO: Handle invalid data format
const response = await handler(ctx, data);
return ctx.json(response);
};
this.registerHandler(endpointDef, handlerWrapper);
}
// Registers a handler for a verb + path combo.
registerHandler(
endpointDef: EndpointDefinition,
handler: (ctx) => any,
) {
this.endpoints.push(endpointDef);
const verb = endpointDef.verb;
const path = endpointDef.path;
switch (verb) {
case "GET":
this.app.get(path, handler);
break;
case "POST":
this.app.post(path, handler);
break;
case "PUT":
this.app.put(path, handler);
break;
case "DELETE":
this.app.delete(path, handler);
break;
case "PATCH":
this.app.patch(path, handler);
break;
default:
throw new Error(`HTTP verb ${verb} not supported`);
}
}
/**
* Usage: `export default gptApi.serve();`
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
9
xkonti avatar

Version 23 changelog: ⚠️ Breaking changes:

  • nothingToJson doesn't take the generic TResponse anymore. The type is inferred from the endpoint definition. The endpoint definition doesn't need the requestSchema/requestDesc defined anymore.
  • jsonToJson doesn't take the generic TRequest and TResponse anymore. Types are inferred from the endpoint definition.

Other changes:

  • Added jsonToNothing that accepts an input, but can respond only with an HTTP code.
xkonti avatar

Version 24 changelog

⚠️ Breaking change: jsonToNothing, nothingToJson and jsonToJson don't accept the request/response descriptions. The descriptions are automatically extracted from request/response schemas.

xkonti avatar

Version 25 changelog

The dto in request handlers has been renamed to reqBody.

xkonti avatar

Version 26 changelog

Added config option policyGetter which takes a function that returns the privacy policy. This will automatically create an API endpoint /policy that will return it on demand. The policy getter is a function so that the policy can be imported on-demand.

xkonti avatar

Version 27 changelog

The policyGetter can be an async function now. This supports passing it functions like this:

... , policyGetter: async () => { const { policy } = await import("https://esm.town/v/user/val"); return policy; },
xkonti avatar

Version 29 changelog

If the provided privacy policy ends with </html>, the /privacypolicy endpoint will return it as HTML content instead of plaintext.

xkonti avatar

Version 32 changelog

  • Merged schema builder into the Val
  • Improved TypeScript type definitions
  • Http verbs are now lowercase
xkonti avatar

Version 36 changelog

  • Added a default GET for / path that deisplays URLs of privacy policy and OpenAPI spec
xkonti avatar

Version 67 changelog

  • Improved documentation
May 29, 2024