gadget/gadget-drone/src/services/ai.ts

152 lines
4.3 KiB
TypeScript

// src/services/ai.ts
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import env from "../config/env.ts";
const aiEnv: IAiEnvironment = {
NODE_ENV: env.NODE_ENV,
services: {
google: {
cse: {
apiKey: env.google.cse.apiKey,
engineId: env.google.cse.engineId,
},
},
},
};
import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
import { GadgetService } from "../lib/service.js";
import {
AiApi,
type IAiChatOptions,
type IAiChatResponse,
type IAiGenerateOptions,
type IAiGenerateResponse,
type IAiModelConfig,
type IAiProvider as AiProviderConfig,
type IAiResponseStreamFn,
createAiApi,
IAiEnvironment,
} from "@gadget/ai";
/**
* An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one
* common interface and contract for working with different AI APIs.
*
* We are Ollama-first and prefer the Ollama API because it offers more actual
* control over the AI models, their performance, and their results at runtime.
*
* OpenAI, however, is the most ubiquitous API, and expands the range of service
* providers developers can use with the Gadget ecosystem.
*/
class AiService extends GadgetService {
get name(): string {
return "AiService";
}
get slug(): string {
return "svc:ai";
}
private activeApi: AiApi | null = null;
async start(): Promise<void> {
this.log.info("started");
}
async stop(): Promise<void> {
this.log.info("stopped");
}
/**
* Converts a database IAiProvider document to a runtime IAiProvider config.
* The DB model uses `apiType` and extends Mongoose Document, while the runtime
* config uses `sdk` and is a plain object.
*/
mapDbProviderToConfig(provider: DbAiProvider | GadgetId): AiProviderConfig {
if (typeof provider === "string") {
throw new Error("Provider must be populated, not a GadgetId reference");
}
return {
_id: provider._id,
name: provider.name,
sdk: provider.apiType, // map apiType → sdk
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
};
}
/**
* Query the list of models available from the provider, then queries the
* models for their individual capabilities. The results are cached in the Gadget
*/
async discovery(provider: DbAiProvider | GadgetId): Promise<void> {
const config = this.mapDbProviderToConfig(provider);
this.log.info("discovering provider model list", {
name: config.name,
sdk: config.sdk,
});
const api = this.getApi(config);
const response = await api.listModels();
this.log.debug("listModels response", { response });
}
async generate(
provider: DbAiProvider | GadgetId,
model: Omit<IAiModelConfig, "provider">,
options: IAiGenerateOptions,
streamCallback?: IAiResponseStreamFn,
): Promise<IAiGenerateResponse> {
const config = this.mapDbProviderToConfig(provider);
this.log.info("calling provider to generate a response", {
name: config.name,
sdk: config.sdk,
haveStreamCallback: !!streamCallback,
options,
});
const api = this.getApi(config);
const modelConfig: IAiModelConfig = { ...model, provider: config };
try {
return await api.generate(modelConfig, options, streamCallback);
} finally {
this.activeApi = null;
}
}
async chat(
provider: DbAiProvider | GadgetId,
model: Omit<IAiModelConfig, "provider">,
options: IAiChatOptions,
streamCallback?: IAiResponseStreamFn,
): Promise<IAiChatResponse> {
const config = this.mapDbProviderToConfig(provider);
this.log.info("calling provider to process chat", {
provider: config.name,
sdk: config.sdk,
model,
haveStreamCallback: !!streamCallback,
});
const api = this.getApi(config);
const modelConfig: IAiModelConfig = { ...model, provider: config };
try {
return await api.chat(modelConfig, options, streamCallback);
} finally {
this.activeApi = null;
}
}
getApi(provider: AiProviderConfig) {
const api = createAiApi(aiEnv, provider, this.log);
this.activeApi = api;
return api;
}
/** Forcefully abort any in-progress request on the active API instance. */
abort(): void {
this.activeApi?.abort();
this.activeApi = null;
}
}
export default new AiService();