198 lines
5.0 KiB
TypeScript
198 lines
5.0 KiB
TypeScript
/**
|
|
* Provider discovery service for AI codegen.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
import * as Context from "effect/Context"
|
|
import * as Data from "effect/Data"
|
|
import * as Effect from "effect/Effect"
|
|
import * as FileSystem from "effect/FileSystem"
|
|
import * as Layer from "effect/Layer"
|
|
import * as Path from "effect/Path"
|
|
import * as Schema from "effect/Schema"
|
|
import * as Yaml from "yaml"
|
|
import { CodegenConfig, type SpecSource, SpecSource as SpecSourceUtils } from "./Config.ts"
|
|
import * as Glob from "./Glob.ts"
|
|
|
|
/**
|
|
* A discovered AI provider with resolved paths.
|
|
*
|
|
* **Example** (Inspecting a discovered provider)
|
|
*
|
|
* ```ts
|
|
* import type * as Discovery from "@effect/ai-codegen/Discovery"
|
|
*
|
|
* declare const provider: Discovery.DiscoveredProvider
|
|
*
|
|
* console.log(provider.name) // "openai"
|
|
* console.log(provider.specSource._tag) // "Url" | "File"
|
|
* ```
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface DiscoveredProvider {
|
|
readonly name: string
|
|
readonly packagePath: string
|
|
readonly config: CodegenConfig
|
|
readonly specSource: SpecSource
|
|
readonly outputPath: string
|
|
}
|
|
|
|
/**
|
|
* Service for discovering AI provider configurations.
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface ProviderDiscovery {
|
|
readonly discover: () => Effect.Effect<
|
|
Array<DiscoveredProvider>,
|
|
DiscoveryError | Glob.GlobError
|
|
>
|
|
readonly discoverOne: (
|
|
name: string
|
|
) => Effect.Effect<
|
|
DiscoveredProvider,
|
|
DiscoveryError | ProviderNotFoundError | Glob.GlobError
|
|
>
|
|
}
|
|
|
|
/**
|
|
* Service tag for discovering AI provider codegen configurations.
|
|
*
|
|
* @category services
|
|
* @since 4.0.0
|
|
*/
|
|
export const ProviderDiscovery: Context.Service<ProviderDiscovery, ProviderDiscovery> = Context.Service(
|
|
"@effect/ai-codegen/ProviderDiscovery"
|
|
)
|
|
|
|
/**
|
|
* Error during provider discovery.
|
|
*
|
|
* **Example** (Creating a discovery error)
|
|
*
|
|
* ```ts
|
|
* import * as Discovery from "@effect/ai-codegen/Discovery"
|
|
*
|
|
* const error = new Discovery.DiscoveryError({
|
|
* message: "Failed to parse config",
|
|
* cause: new Error("Invalid JSON")
|
|
* })
|
|
* ```
|
|
*
|
|
* @category errors
|
|
* @since 4.0.0
|
|
*/
|
|
export class DiscoveryError extends Data.TaggedError("DiscoveryError")<{
|
|
readonly message: string
|
|
readonly cause?: unknown
|
|
}> {}
|
|
|
|
/**
|
|
* Error when a specific provider is not found.
|
|
*
|
|
* **Example** (Creating a provider not found error)
|
|
*
|
|
* ```ts
|
|
* import * as Discovery from "@effect/ai-codegen/Discovery"
|
|
*
|
|
* const error = new Discovery.ProviderNotFoundError({
|
|
* provider: "openai",
|
|
* available: ["anthropic", "google"]
|
|
* })
|
|
* ```
|
|
*
|
|
* @category errors
|
|
* @since 4.0.0
|
|
*/
|
|
export class ProviderNotFoundError extends Data.TaggedError("ProviderNotFoundError")<{
|
|
readonly provider: string
|
|
readonly available: ReadonlyArray<string>
|
|
}> {}
|
|
|
|
/**
|
|
* Layer providing the ProviderDiscovery service.
|
|
*
|
|
* @category layers
|
|
* @since 4.0.0
|
|
*/
|
|
export const layer: Layer.Layer<
|
|
ProviderDiscovery,
|
|
never,
|
|
Glob.Glob | FileSystem.FileSystem | Path.Path
|
|
> = Effect.gen(function*() {
|
|
const glob = yield* Glob.Glob
|
|
const fs = yield* FileSystem.FileSystem
|
|
const pathService = yield* Path.Path
|
|
|
|
const parseConfig = Effect.fn("parseConfig")(function*(configPath: string) {
|
|
const packagePath = pathService.dirname(configPath)
|
|
const name = pathService.basename(packagePath)
|
|
const isYaml = configPath.endsWith(".yaml") || configPath.endsWith(".yml")
|
|
|
|
const content = yield* fs.readFileString(configPath).pipe(
|
|
Effect.mapError((cause) =>
|
|
new DiscoveryError({
|
|
message: `Failed to read config at ${configPath}`,
|
|
cause
|
|
})
|
|
)
|
|
)
|
|
|
|
const parsed = yield* Effect.try({
|
|
try: () => isYaml ? Yaml.parse(content) : JSON.parse(content),
|
|
catch: (cause) =>
|
|
new DiscoveryError({
|
|
message: `Failed to parse ${isYaml ? "YAML" : "JSON"} at ${configPath}`,
|
|
cause
|
|
})
|
|
})
|
|
|
|
const config = yield* Schema.decodeUnknownEffect(CodegenConfig)(parsed).pipe(
|
|
Effect.mapError((cause) =>
|
|
new DiscoveryError({
|
|
message: `Invalid config schema at ${configPath}`,
|
|
cause
|
|
})
|
|
)
|
|
)
|
|
|
|
const provider: DiscoveredProvider = {
|
|
name,
|
|
packagePath,
|
|
config,
|
|
specSource: SpecSourceUtils.fromConfig(config.spec, packagePath, pathService),
|
|
outputPath: pathService.join(packagePath, config.output)
|
|
}
|
|
|
|
return provider
|
|
})
|
|
|
|
const discover = Effect.fn("discover")(function*() {
|
|
const configFiles = yield* glob.glob("packages/ai/*/codegen.{json,yaml,yml}", {
|
|
cwd: process.cwd(),
|
|
absolute: true
|
|
})
|
|
|
|
return yield* Effect.forEach(configFiles, parseConfig)
|
|
})
|
|
|
|
const discoverOne = Effect.fn("discoverOne")(function*(providerName: string) {
|
|
const providers = yield* discover()
|
|
const found = providers.find((p) => p.name === providerName)
|
|
|
|
if (!found) {
|
|
return yield* new ProviderNotFoundError({
|
|
provider: providerName,
|
|
available: providers.map((p) => p.name)
|
|
})
|
|
}
|
|
|
|
return found
|
|
})
|
|
|
|
return { discover, discoverOne }
|
|
}).pipe(Layer.effect(ProviderDiscovery))
|