Zum Hauptinhalt springen
TypeChat

Grundlegende TypeScript-Nutzung

TypeChat ist derzeit eine kleine Bibliothek, daher werfen wir einen Blick auf einige grundlegende Verwendungsmöglichkeiten, um sie zu verstehen.

import fs from "fs";
import path from "path";
import { createJsonTranslator, createLanguageModel } from "typechat";
import { processRequests } from "typechat/interactive";
import { createTypeScriptJsonValidator } from "typechat/ts";
import { SentimentResponse } from "./sentimentSchema";

// Create a model.
const model = createLanguageModel(process.env);

// Create a validator.
const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8");
const validator = createTypeScriptJsonValidator<SentimentResponse>(schema, "SentimentResponse");

// Create a translator.
const translator = createJsonTranslator(model, validator);

// Process requests interactively or from the input file specified on the command line
processRequests("😀> ", process.argv[2], async (request) => {
    const response = await translator.translate(request);
    if (!response.success) {
        console.log(response.message);
        return;
    }
    console.log(`The sentiment is ${response.data.sentiment}`);
});

Bereitstellung eines Modells

TypeChat kann mit jedem Sprachmodell verwendet werden. Solange Sie ein Objekt mit den folgenden Eigenschaften erstellen können

export interface TypeChatLanguageModel {
    /**
     * Optional property that specifies the maximum number of retry attempts (the default is 3).
     */
    retryMaxAttempts?: number;
    /**
     * Optional property that specifies the delay before retrying in milliseconds (the default is 1000ms).
     */
    retryPauseMs?: number;
    /**
     * Obtains a completion from the language model for the given prompt.
     * @param prompt The prompt string.
     */
    complete(prompt: string): Promise<Result<string>>;
}

dann sollten Sie TypeChat mit einem solchen Modell ausprobieren können.

Das Wichtigste hierbei ist, dass nur complete erforderlich ist. complete ist einfach eine Funktion, die einen string entgegennimmt und bei Erfolg schließlich einen string zurückgibt.

Zur Vereinfachung bietet TypeChat zwei vorgefertigte Funktionen, um eine Verbindung zur OpenAI-API und zu den Azure OpenAI Services herzustellen. Sie können diese direkt aufrufen.

export function createOpenAILanguageModel(apiKey: string, model: string, endPoint? string): TypeChatLanguageModel;

export function createAzureOpenAILanguageModel(apiKey: string, endPoint: string): TypeChatLanguageModel;

Für noch mehr Komfort bietet TypeChat auch eine Funktion, um zu erkennen, ob Sie OpenAI oder Azure OpenAI verwenden.

export function createLanguageModel(env: Record<string, string | undefined>): TypeChatLanguageModel

Sie können Ihre Umgebungsvariablen befüllen, und je nachdem, ob OPENAI_API_KEY oder AZURE_OPENAI_API_KEY gesetzt ist, erhalten Sie ein Modell des entsprechenden Typs.

import dotenv from "dotenv";
dotenv.config(/*...*/);
import * as typechat from "typechat";
const model = typechat.createLanguageModel(process.env);

Unabhängig davon, wie Sie Ihr Modell erstellen, empfehlen wir, Ihre geheimen Token/API-Schlüssel in einer .env-Datei aufzubewahren und .env in einer .gitignore anzugeben. Sie können eine Bibliothek wie dotenv verwenden, um diese zu laden.

Laden des Schemas

TypeChat beschreibt Typen für Sprachmodelle, um deren Antworten zu steuern. In diesem Fall verwenden wir einen TypeScriptJsonValidator, der den TypeScript-Compiler verwendet, um Daten gegen eine Reihe von Typen zu validieren. Das bedeutet, dass wir die Typen der Daten, die wir zurückerwarten, in einer .ts-Datei niederschreiben werden. Hier ist, wie unsere Schema-Datei sentimentSchema.ts aussehen würde

// The following is a schema definition for determining the sentiment of a some user input.

export interface SentimentResponse {
    sentiment: "negative" | "neutral" | "positive";  // The sentiment of the text
}

Das bedeutet auch, dass wir eine Eingabe-.ts-Datei wörtlich laden müssen.

// Load up the type from our schema.
import type { SentimentResponse } from "./sentimentSchema";

// Load up the schema file contents.
const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8");

Hinweis: Dieser Code geht von einem CommonJS-Modul aus. Wenn Sie ECMAScript-Module verwenden, können Sie je nach Version Ihrer Laufzeitumgebung import.meta.url oder über import.meta.dirname verwenden.

Dies führt zu einigen Komplikationen bei bestimmten Build-Arten, da unsere Eingabedateien als lokale Assets behandelt werden müssen. Eine Möglichkeit, dies zu erreichen, ist die Verwendung einer Laufzeitumgebung oder eines Tools wie ts-node, um sowohl die Datei für ihre Typen zu importieren als auch den Dateiinhalt zu lesen. Eine andere Möglichkeit ist die Verwendung eines Dienstprogramms wie copyfiles, um bestimmte Schema-Dateien in das Ausgabeverzeichnis zu verschieben. Wenn Sie einen Bundler verwenden, gibt es möglicherweise eine benutzerdefinierte Möglichkeit, eine Datei als Roh-String zu importieren. Unabhängig davon sollten unsere Beispiele mit einer der beiden ersten Optionen funktionieren.

Alternativ können wir, wenn wir möchten, unser Schema vollständig im Speicher mit Zod und einem ZodValidator erstellen, auf den wir gleich eingehen werden. Hier ist, wie unser Schema aussehen würde, wenn wir diesen Weg gehen würden.

import { z } from "zod";

export const SentimentResponse = z.object({
    sentiment: z.enum(["negative", "neutral", "positive"]).describe("The sentiment of the text")
});

export const SentimentSchema = {
    SentimentResponse
};

Erstellung eines Validators

Ein Validator hat im Grunde zwei Aufgaben: die Generierung eines textuellen Schemas für Sprachmodelle und die Sicherstellung, dass alle Daten einer bestimmten Form entsprechen. Die Schnittstelle sieht ungefähr so aus:

/**
 * An object that represents a TypeScript schema for JSON objects.
 */
export interface TypeChatJsonValidator<T extends object> {
    /**
     * Return a string containing TypeScript source code for the validation schema.
     */
    getSchemaText(): string;
    /**
     * Return the name of the JSON object target type in the schema.
     */
    getTypeName(): string;
    /**
     * Validates the given JSON object according to the associated TypeScript schema. Returns a
     * `Success<T>` object containing the JSON object if validation was successful. Otherwise, returns
     * an `Error` object with a `message` property describing the error.
     * @param jsonText The JSON object to validate.
     * @returns The JSON object or an error message.
     */
    validate(jsonObject: object): Result<T>;
}

Mit anderen Worten, dies sind nur der Text aller Typen, der Name des Top-Level-Typs, mit dem geantwortet werden soll, und eine Validierungsfunktion, die bei Erfolg eine stark typisierte Ansicht der Eingabe zurückgibt.

TypeChat wird mit zwei Validatoren ausgeliefert.

TypeScriptJsonValidator

Ein TypeScriptJsonValidator arbeitet mit TypeScript-Textdateien. Um einen zu erstellen, müssen wir createTypeScriptJsonValidator aus typechat/ts importieren

import { createTypeScriptJsonValidator } from "typechat/ts";

Wir müssen auch den Typ aus unserem Schema importieren.

import { SentimentResponse } from "./sentimentSchema";

Mit unserem Schematext und diesem Typ haben wir genug, um einen Validator zu erstellen

const validator = createTypeScriptJsonValidator<SentimentResponse>(schema, "SentimentResponse");

Wir haben den Text des Schemas und den Namen des Typs angegeben, den die zurückgegebenen Daten erfüllen sollen. Wir müssen auch das Typ-Argument SentimentResponse angeben, um die erwartete Datenform zu erklären (obwohl dies wie ein Typ-Cast ist und nicht garantiert wird).

Zod-Validatoren

Wenn Sie Ihr Schema mit Zod definieren, können Sie die Funktion createZodJsonValidator verwenden

import { createZodJsonValidator } from "typechat/zod";

Anstelle einer Quelldatei benötigt ein Zod-Validator ein JavaScript-Objekt, das von Typnamen auf Zod-Typ-Objekte abgebildet wird, wie myObj im folgenden Beispiel

export const MyType = z.object(/*...*/);

export const MyOtherType = z.object(/*...*/);

export let myObj = {
    MyType,
    MyOtherType,
}

Wie oben war das nur SentimentSchema

export const SentimentSchema = {
    SentimentResponse
};

Wir müssen dieses Objekt also importieren...

import { SentimentSchema } from "./sentimentSchema";

und es zusammen mit unserem erwarteten Typnamen an createZodJsonValidator übergeben

const validator = createZodJsonValidator(SentimentSchema, "SentimentResponse");

Erstellung eines JSON-Übersetzers

Ein TypeChatJsonTranslator bringt diese zusammen.

import { createJsonTranslator } from "typechat";

Ein Übersetzer nimmt sowohl ein Modell als auch einen Validator entgegen und bietet eine Möglichkeit, Benutzereingaben in Objekte innerhalb unseres Schemas zu übersetzen. Dazu erstellt er einen Prompt basierend auf dem Schema, kontaktiert das Modell, parst JSON-Daten und versucht die Validierung. Optional erstellt er Reparatur-Prompts und versucht es erneut, wenn die Validierung fehlschlägt.

const translator = createJsonTranslator(model, validator);

Wenn wir bereit sind, eine Benutzeranfrage zu übersetzen, können wir die Methode translate aufrufen.

translator.translate("Hello world! 🙂");

Darauf kommen wir zurück.

Erstellung des Prompts

TypeChat exportiert eine Funktion namens processRequests, die das Experimentieren mit TypeChat erleichtert. Wir müssen sie aus typechat/interactive importieren.

import { processRequests } from "typechat/interactive";

Sie erstellt entweder eine interaktive Kommandozeilenaufforderung oder liest Zeilen aus einer Datei.

typechat.processRequests("😀> ", process.argv[2], async (request) => {
    // ...
});

processRequests nimmt 3 Dinge entgegen. Erstens gibt es das Prompt-Präfix – das ist das, was ein Benutzer vor seinem eigenen Text in interaktiven Szenarien sieht. Sie können dies spielerisch gestalten. Wir verwenden hier gerne Emojis. 😄

Als Nächstes nehmen wir einen Dateinamen entgegen. Eingabezeichenfolgen werden zeilenweise aus dieser Datei gelesen. Wenn der Dateiname undefined ist, arbeitet processRequests mit Standardeingabe und stellt eine interaktive Aufforderung bereit. Die Verwendung von process.argv[2] macht unser Programm standardmäßig interaktiv, es sei denn, die ausführende Person hat eine Eingabedatei als Kommandozeilenargument angegeben (z. B. node ./dist/main.js inputFile.txt).

Schließlich gibt es den Request-Handler. Den füllen wir als Nächstes.

Übersetzen von Anfragen

Unser Handler empfängt bei jedem Aufruf eine Benutzereingabe (den request-String). Es ist an der Zeit, diesen String an unser translator-Objekt weiterzuleiten.

typechat.processRequests("😀> ", process.argv[2], async (request) => {
    const response = await translator.translate(request);
    if (!response.success) {
        console.log(response.message);
        return;
    }
    console.log(`The sentiment is ${response.data.sentiment}`);
});

Wir rufen die Methode translate für jeden String auf und erhalten eine Antwort. Wenn etwas schief geht, wird TypeChat Anfragen bis zu einem von retryMaxAttempts auf unserem model festgelegten Maximum wiederholen. Wenn jedoch die anfängliche Anfrage und alle Wiederholungen fehlschlagen, ist response.success false und wir können eine message abrufen, die erklärt, was schiefgelaufen ist.

Im Idealfall ist response.success true und wir können auf unsere gut typisierte data-Eigenschaft zugreifen! Diese entspricht dem Typ, den wir beim Erstellen unseres Translator-Objekts übergeben haben (d. h. SentimentResponse).

Das ist alles! Sie sollten nun eine grundlegende Vorstellung von den APIs von TypeChat und den ersten Schritten für ein neues Projekt haben. 🎉