@genkit-ai/core
Version:
Genkit AI framework core libraries.
338 lines • 11.1 kB
JavaScript
import express from "express";
import fs from "fs/promises";
import getPort, { makeRange } from "get-port";
import path from "path";
import * as z from "zod";
import { StatusCodes } from "./action.js";
import { GENKIT_REFLECTION_API_SPEC_VERSION, GENKIT_VERSION } from "./index.js";
import { logger } from "./logging.js";
import { toJsonSchema } from "./schema.js";
import { flushTracing, setTelemetryServerUrl } from "./tracing.js";
const RunActionResponseSchema = z.object({
result: z.unknown().optional(),
error: z.unknown().optional(),
telemetry: z.object({
traceId: z.string().optional()
}).optional()
});
class ReflectionServer {
/** List of all running servers needed to be cleaned up on process exit. */
static RUNNING_SERVERS = [];
/** Registry instance to be used for API calls. */
registry;
/** Options for the reflection server. */
options;
/** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null if server is not running. */
port = null;
/** Express server instance. Null if server is not running. */
server = null;
/** Path to the runtime file. Null if server is not running. */
runtimeFilePath = null;
constructor(registry, options) {
this.registry = registry;
this.options = {
port: 3100,
bodyLimit: "30mb",
configuredEnvs: ["dev"],
...options
};
}
/**
* Finds a free port to run the server on based on the original chosen port and environment.
*/
async findPort() {
const chosenPort = this.options.port;
const freePort = await getPort({
port: makeRange(chosenPort, chosenPort + 100)
});
if (freePort !== chosenPort) {
logger.warn(
`Port ${chosenPort} is already in use, using next available port ${freePort} instead.`
);
}
return freePort;
}
/**
* Starts the server.
*
* The server will be registered to be shut down on process exit.
*/
async start() {
const server = express();
server.use(express.json({ limit: this.options.bodyLimit }));
server.use((req, res, next) => {
res.header("x-genkit-version", GENKIT_VERSION);
next();
});
server.get("/api/__health", async (_, response) => {
await this.registry.listActions();
response.status(200).send("OK");
});
server.get("/api/__quitquitquit", async (_, response) => {
logger.debug("Received quitquitquit");
response.status(200).send("OK");
await this.stop();
});
server.get("/api/actions", async (_, response, next) => {
logger.debug("Fetching actions.");
try {
const actions = await this.registry.listResolvableActions();
const convertedActions = {};
Object.keys(actions).forEach((key) => {
const action = actions[key];
convertedActions[key] = {
key,
name: action.name,
description: action.description,
metadata: action.metadata
};
if (action.inputSchema || action.inputJsonSchema) {
convertedActions[key].inputSchema = toJsonSchema({
schema: action.inputSchema,
jsonSchema: action.inputJsonSchema
});
}
if (action.outputSchema || action.outputJsonSchema) {
convertedActions[key].outputSchema = toJsonSchema({
schema: action.outputSchema,
jsonSchema: action.outputJsonSchema
});
}
});
response.send(convertedActions);
} catch (err) {
const { message, stack } = err;
next({ message, stack });
}
});
server.post("/api/runAction", async (request, response, next) => {
const { key, input, context, telemetryLabels } = request.body;
const { stream } = request.query;
logger.debug(`Running action \`${key}\` with stream=${stream}...`);
try {
const action = await this.registry.lookupAction(key);
if (!action) {
response.status(404).send(`action ${key} not found`);
return;
}
if (stream === "true") {
try {
const callback = (chunk) => {
response.write(JSON.stringify(chunk) + "\n");
};
const result = await action.run(input, {
context,
onChunk: callback,
telemetryLabels
});
await flushTracing();
response.write(
JSON.stringify({
result: result.result,
telemetry: {
traceId: result.telemetry.traceId
}
})
);
response.end();
} catch (err) {
const { message, stack } = err;
const errorResponse = {
code: StatusCodes.INTERNAL,
message,
details: {
stack
}
};
if (err.traceId) {
errorResponse.details.traceId = err.traceId;
}
response.write(
JSON.stringify({
error: errorResponse
})
);
response.end();
}
} else {
const result = await action.run(input, { context, telemetryLabels });
await flushTracing();
response.send({
result: result.result,
telemetry: {
traceId: result.telemetry.traceId
}
});
}
} catch (err) {
const { message, stack, traceId } = err;
next({ message, stack, traceId });
}
});
server.get("/api/envs", async (_, response) => {
response.json(this.options.configuredEnvs);
});
server.post("/api/notify", async (request, response) => {
const { telemetryServerUrl, reflectionApiSpecVersion } = request.body;
if (!process.env.GENKIT_TELEMETRY_SERVER) {
if (typeof telemetryServerUrl === "string") {
setTelemetryServerUrl(telemetryServerUrl);
logger.debug(
`Connected to telemetry server on ${telemetryServerUrl}`
);
}
}
if (reflectionApiSpecVersion !== GENKIT_REFLECTION_API_SPEC_VERSION) {
if (!reflectionApiSpecVersion || reflectionApiSpecVersion < GENKIT_REFLECTION_API_SPEC_VERSION) {
logger.warn(
"WARNING: Genkit CLI version may be outdated. Please update `genkit-cli` to the latest version."
);
} else {
logger.warn(
`Genkit CLI is newer than runtime library. Some feature may not be supported. Consider upgrading your runtime library version (debug info: expected ${GENKIT_REFLECTION_API_SPEC_VERSION}, got ${reflectionApiSpecVersion}).`
);
}
}
response.status(200).send("OK");
});
server.use((err, req, res, next) => {
logger.error(err.stack);
const error = err;
const { message, stack } = error;
const errorResponse = {
code: StatusCodes.INTERNAL,
message,
details: {
stack
}
};
if (err.traceId) {
errorResponse.details.traceId = err.traceId;
}
res.status(500).json(errorResponse);
});
this.port = await this.findPort();
this.server = server.listen(this.port, async () => {
logger.debug(
`Reflection server (${process.pid}) running on http://localhost:${this.port}`
);
ReflectionServer.RUNNING_SERVERS.push(this);
await this.writeRuntimeFile();
});
}
/**
* Stops the server and removes it from the list of running servers to clean up on exit.
*/
async stop() {
if (!this.server) {
return;
}
return new Promise(async (resolve, reject) => {
await this.cleanupRuntimeFile();
this.server.close(async (err) => {
if (err) {
logger.error(
`Error shutting down reflection server on port ${this.port}: ${err}`
);
reject(err);
}
const index = ReflectionServer.RUNNING_SERVERS.indexOf(this);
if (index > -1) {
ReflectionServer.RUNNING_SERVERS.splice(index, 1);
}
logger.debug(
`Reflection server on port ${this.port} has successfully shut down.`
);
this.port = null;
this.server = null;
resolve();
});
});
}
/**
* Writes the runtime file to the project root.
*/
async writeRuntimeFile() {
try {
const rootDir = await findProjectRoot();
const runtimesDir = path.join(rootDir, ".genkit", "runtimes");
const date = /* @__PURE__ */ new Date();
const time = date.getTime();
const timestamp = date.toISOString();
const runtimeId = `${process.pid}${this.port !== null ? `-${this.port}` : ""}`;
this.runtimeFilePath = path.join(
runtimesDir,
`${runtimeId}-${time}.json`
);
const fileContent = JSON.stringify(
{
id: process.env.GENKIT_RUNTIME_ID || runtimeId,
pid: process.pid,
name: this.options.name,
reflectionServerUrl: `http://localhost:${this.port}`,
timestamp,
genkitVersion: `nodejs/${GENKIT_VERSION}`,
reflectionApiSpecVersion: GENKIT_REFLECTION_API_SPEC_VERSION
},
null,
2
);
await fs.mkdir(runtimesDir, { recursive: true });
await fs.writeFile(this.runtimeFilePath, fileContent, "utf8");
logger.debug(`Runtime file written: ${this.runtimeFilePath}`);
} catch (error) {
logger.error(`Error writing runtime file: ${error}`);
}
}
/**
* Cleans up the port file.
*/
async cleanupRuntimeFile() {
if (!this.runtimeFilePath) {
return;
}
try {
const fileContent = await fs.readFile(this.runtimeFilePath, "utf8");
const data = JSON.parse(fileContent);
if (data.pid === process.pid) {
await fs.unlink(this.runtimeFilePath);
logger.debug(`Runtime file cleaned up: ${this.runtimeFilePath}`);
}
} catch (error) {
logger.error(`Error cleaning up runtime file: ${error}`);
}
}
/**
* Stops all running reflection servers.
*/
static async stopAll() {
return Promise.all(
ReflectionServer.RUNNING_SERVERS.map((server) => server.stop())
);
}
}
async function findProjectRoot() {
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const packageJsonPath = path.join(currentDir, "package.json");
try {
await fs.access(packageJsonPath);
return currentDir;
} catch {
currentDir = path.dirname(currentDir);
}
}
throw new Error("Could not find project root (package.json not found)");
}
if (typeof module !== "undefined" && "hot" in module) {
module.hot.accept();
module.hot.dispose(async () => {
logger.debug("Cleaning up reflection server(s) before module reload...");
await ReflectionServer.stopAll();
});
}
export {
ReflectionServer,
RunActionResponseSchema
};
//# sourceMappingURL=reflection.mjs.map