Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

types generator #27

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

# 0.4.3

- Langtail types generator

# 0.4.2

- list deployments EP
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,8 @@ tools(ltModel, {
},
},
})
```
```

## Typed inputs

You can override input types to improve IntelliSense for the `prompt`, `environment`, and `version` attributes. Use the command `npx langtail generate-types`.
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "langtail",
"version": "0.4.2",
"version": "0.4.3",
"description": "",
"main": "./dist/LangtailNode.js",
"packageManager": "pnpm@8.15.6",
Expand All @@ -14,7 +14,7 @@
"test": "vitest",
"ts": "tsc --noEmit",
"format": "prettier --write .",
"build": "tsup && copyfiles -u 1 src/bin/langtailTools.template.ts dist/",
"build": "tsup && copyfiles -u 1 src/bin/*.template dist/",
"prepublishOnly": "pnpm run build"
},
"keywords": [
Expand Down Expand Up @@ -63,6 +63,11 @@
"require": "./dist/vercelAi/index.js",
"import": "./dist/vercelAi/index.mjs",
"types": "./dist/vercelAi/index.d.ts"
},
"./dist/customTypes": {
"require": "./dist/customTypes.js",
"import": "./dist/customTypes.mjs",
"types": "./dist/customTypes.d.ts"
}
},
"files": [
Expand Down Expand Up @@ -91,13 +96,15 @@
"clean": true,
"entryPoints": [
"src/LangtailNode.ts",
"src/customTypes.ts",
"src/template.ts",
"src/getOpenAIBody.ts",
"src/vercelAi/index.ts",
"src/bin/entry.ts"
],
"external": [
"dotenv-flow"
"dotenv-flow",
"langtail/dist/customTypes"
]
}
}
70 changes: 28 additions & 42 deletions src/LangtailPrompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,25 @@ import { userAgent } from "./userAgent"
import queryString from "query-string"
import { Deployment, PlaygroundState } from "./schemas"
import { OpenAiBodyType, getOpenAIBody } from "./getOpenAIBody"

export type LangtailEnvironment = "preview" | "staging" | "production"
import { Environment, PromptOptions, PromptSlug, Version, LangtailEnvironment } from "./types"

interface LangtailPromptVariables { } // TODO use this when generating schema for deployed prompts

export { LangtailEnvironment }

type StreamResponseType = Stream<ChatCompletionChunk>

type OpenAIResponseWithHttp = ChatCompletion & {
httpResponse: Response | globalThis.Response
}

interface CreatePromptPathOptions<P extends PromptSlug, E extends Environment<P> & LangtailEnvironment, V extends Version<P, E>> {
prompt: P,
environment: E,
version?: V,
configGet?: boolean
}

type Options = {
apiKey: string
baseURL?: string | undefined
Expand All @@ -31,22 +39,14 @@ type Options = {
onResponse?: (response: ChatCompletion) => void
}

interface IPromptIdProps extends ILangtailExtraProps, OpenAiBodyType {
prompt: string
/**
* The environment to fetch the prompt from. Defaults to "production".
* @default "production"
**/
environment?: LangtailEnvironment
version?: string
}
type IPromptIdProps<P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined> = PromptOptions<P, E, V> & ILangtailExtraProps & OpenAiBodyType

export interface IRequestParams extends IPromptIdProps {
export type IRequestParams<P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined> = IPromptIdProps<P, E, V> & {
variables?: Record<string, any>
}

interface IRequestParamsStream extends IRequestParams {
stream: boolean
type IRequestParamsStream<P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined, S extends boolean | undefined = false> = IRequestParams<P, E, V> & {
stream?: S
}

export class LangtailPrompts {
Expand All @@ -61,17 +61,12 @@ export class LangtailPrompts {
this.options = options
}

createPromptPath({
createPromptPath<P extends PromptSlug, E extends Environment<P> & LangtailEnvironment, V extends Version<P, E>>({
prompt,
environment,
version,
configGet,
}: {
prompt: string
environment: LangtailEnvironment
version?: string
configGet?: boolean
}) {
}: CreatePromptPathOptions<P, E, V>) {
if (prompt.includes("/")) {
throw new Error(
"prompt should not include / character, either omit workspace/project or use just the prompt name.",
Expand Down Expand Up @@ -100,18 +95,17 @@ export class LangtailPrompts {
: `${this.baseUrl}/${urlPath}${queryParamsString}`
}

invoke(
options: Omit<IRequestParams, "stream">,
): Promise<OpenAIResponseWithHttp>
invoke(options: IRequestParamsStream): Promise<StreamResponseType>
async invoke({
async invoke<P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined, S extends boolean = false>({
prompt,
environment,
version,
doNotRecord,
metadata,
stream,
...rest
}: IRequestParams | IRequestParamsStream) {
}: IRequestParamsStream<P, E, V, S>): Promise<S extends true ? StreamResponseType : OpenAIResponseWithHttp> {
type OutputType = S extends true ? StreamResponseType : OpenAIResponseWithHttp

const metadataHeaders = metadata
? Object.entries(metadata).reduce((acc, [key, value]) => {
acc[`x-langtail-metadata-${key}`] = value
Expand All @@ -128,7 +122,7 @@ export class LangtailPrompts {
"x-langtail-do-not-record": doNotRecord ? "true" : "false",
...metadataHeaders,
},
body: JSON.stringify({ stream: false, ...rest }),
body: JSON.stringify({ stream, ...rest }),
}
const promptPath = this.createPromptPath({
prompt,
Expand All @@ -150,22 +144,22 @@ export class LangtailPrompts {
)
}

if ("stream" in rest && rest.stream) {
if (stream) {
if (!res.body) {
throw new Error("No body in response")
}
return Stream.fromSSEResponse(res, new AbortController())
return Stream.fromSSEResponse(res, new AbortController()) as OutputType
}

const result = (await res.json()) as OpenAIResponseWithHttp
if (this.options.onResponse) {
this.options.onResponse(result)
}
result.httpResponse = res
return result
return result as OutputType
}

async listDeployments(): Promise<Deployment[]> {
async listDeployments<P extends PromptSlug>(): Promise<Deployment<P>[]> {
const res = await fetch(`${this.baseUrl}/list-deployments`, {
headers: {
"X-API-Key": this.apiKey,
Expand All @@ -184,19 +178,11 @@ export class LangtailPrompts {
return responseJson.deployments
}

async get({
async get<P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined>({
prompt,
environment,
version,
}: {
prompt: string
/**
* The environment to fetch the prompt from. Defaults to "production".
* @default "production"
**/
environment?: LangtailEnvironment
version?: string
}): Promise<PlaygroundState> {
}: PromptOptions<P, E, V>): Promise<PlaygroundState> {
const promptPath = this.createPromptPath({
prompt,
environment: environment ?? "production",
Expand Down
22 changes: 11 additions & 11 deletions src/bin/entry.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import 'dotenv-flow/config'
import { program } from 'commander';
import generateTools from './generateTools';
import packageJson from "../../package.json";

import generateTools, { determineDefaultPath as determineDefaultPathTools } from './generateTools';
import generateTypes, { determineDefaultPath as determineDefaultPathTypes } from './generateTypes';
import SDK_VERSION from '../version'

function actionErrorHanlder(error: Error) {
console.error(error.message);
Expand All @@ -18,16 +16,18 @@ export function actionRunner(fn: (...args) => Promise<any>) {
}

program
.version(packageJson.version);
.version(SDK_VERSION);

function determineDefaultPath() {
return fs.existsSync(path.join(process.cwd(), 'src')) ? 'src/langtailTools.ts' : 'langtailTools.ts';
}
program
.command('generate-types')
.description('Generate types for your Langtail project')
.option('--out [path]', 'output file path', determineDefaultPathTypes())
.action(actionRunner(generateTypes));

program
.command('generate-tools')
.description('Generate tools based on a Langtail prompt')
.option('--out [path]', 'output file path', determineDefaultPath())
.description('Generate tools based on Langtail prompts in your project')
.option('--out [path]', 'output file path', determineDefaultPathTools())
.action(actionRunner(generateTools));

program.parse(process.argv);
77 changes: 14 additions & 63 deletions src/bin/generateTools.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import readline from 'readline';
import { LangtailEnvironment, LangtailPrompts } from '../LangtailPrompts';
import { LangtailPrompts } from '../LangtailPrompts';
import jsonSchemaToZod from 'json-schema-to-zod';
import packageJson from "../../package.json"
import SDK_VERSION from '../version'
import { askUserToConfirm, dirExists, getApiKey, prepareOutputFilePath } from './utils';
import { Environment, PromptOptions, PromptSlug, Version } from '../types';

const SDK_VERSION = packageJson.version;
const TEMPLATE_PATH = new URL('./langtailTools.template.ts', import.meta.url);
const REPLACE_LINE = 'const toolsObject = {}; // replaced by generateTools.ts'

const getApiKey = (): string => {
const apiKey = process.env.LANGTAIL_API_KEY;
if (!apiKey) {
throw new Error('LANGTAIL_API_KEY environment variable is required');
}
return apiKey;
}
const DEFAULT_FILENAME = 'langtailTools.ts';
const TEMPLATE_PATH = new URL('./langtailTools.ts.template', import.meta.url);
const REPLACE_LINE = 'const toolsObject = {}; // replaced by generateTools.ts'

interface FetchToolsOptions {
interface FetchToolsOptions<P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined> extends PromptOptions<P, E, V> {
langtailPrompts: LangtailPrompts;
promptSlug: string;
environment: LangtailEnvironment;
version: string | undefined;
}

interface Tools {
Expand All @@ -32,7 +22,7 @@ interface Tools {
}
}

const fetchTools = async ({ langtailPrompts, promptSlug, environment, version }: FetchToolsOptions): Promise<Tools | undefined> => {
const fetchTools = async <P extends PromptSlug, E extends Environment<P> = undefined, V extends Version<P, E> = undefined>({ langtailPrompts, prompt: promptSlug, environment, version }: FetchToolsOptions<P, E, V>): Promise<Tools | undefined> => {
const prompt = await langtailPrompts.get({
prompt: promptSlug,
environment: environment,
Expand All @@ -49,47 +39,6 @@ const fetchTools = async ({ langtailPrompts, promptSlug, environment, version }:
}
}

function askUserToConfirm(query: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

return new Promise(resolve => {
rl.question(query, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}

const prepareOutputFilePath = async (outputFile: string): Promise<string | undefined> => {
let resultFilePath = outputFile;
if (fs.existsSync(resultFilePath) && fs.statSync(resultFilePath).isDirectory()) {
resultFilePath = path.join(resultFilePath, 'langtailTools.ts');
}

if (fs.existsSync(resultFilePath)) {
const confirmed = await askUserToConfirm(`File ${resultFilePath} exists. Overwrite? [y/N]: `);
if (!confirmed) {
return;
}
}

const directory = path.dirname(resultFilePath);
if (!fs.existsSync(directory)) {
const confirmed = await askUserToConfirm(`Directory ${directory} does not exist. Create it? [y/N]: `);
if (confirmed) {
fs.mkdirSync(directory, { recursive: true });
console.log(`Created directory: ${directory}`);
} else {
return;
}
}

return resultFilePath;
}

const stringifyToolsObject = (obj: object, depth = 0): string => {
const indent = ' '.repeat(depth);
let result = '{\n';
Expand Down Expand Up @@ -126,12 +75,14 @@ interface ToolsObject {
}
}

export const determineDefaultPath = () => dirExists('src') ? `src/${DEFAULT_FILENAME}` : DEFAULT_FILENAME;

interface GenerateToolsOptions {
out: string;
}

const generateTools = async ({ out }: GenerateToolsOptions) => {
const outputFile = await prepareOutputFilePath(out);
const outputFile = await prepareOutputFilePath(out, DEFAULT_FILENAME);
if (!outputFile) {
console.log('Operation cancelled by the user.');
return;
Expand All @@ -147,8 +98,8 @@ const generateTools = async ({ out }: GenerateToolsOptions) => {
for (const deployment of deployments) {
const { promptSlug, environment, version } = deployment;
try {
const promptTools = await fetchTools({ langtailPrompts, promptSlug, environment: environment as LangtailEnvironment, version });
if (promptTools) {
const promptTools = await fetchTools({ langtailPrompts, prompt: promptSlug, environment: environment, version });
if (promptTools && environment && promptSlug) {
toolsObject[promptSlug] = toolsObject[promptSlug] || {};
toolsObject[promptSlug][environment] = toolsObject[promptSlug][environment] || {};
if (version) {
Expand Down
Loading