Skip to content

Commit

Permalink
Merge pull request #29 from langtail/types
Browse files Browse the repository at this point in the history
Typings generator
  • Loading branch information
petrbrzek committed Jun 19, 2024
2 parents c04a08c + 69cf9f6 commit ca4ef93
Show file tree
Hide file tree
Showing 19 changed files with 352 additions and 148 deletions.
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,7 @@ 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: stream === true, ...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

0 comments on commit ca4ef93

Please sign in to comment.