From 48abee55baa3cf5ac4070ba1d940de2d67de83bc Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 15:53:46 -0700 Subject: [PATCH 01/16] feat: add hub js sdk to langsmith js sdk --- js/src/client.ts | 565 ++++++++++++++++++ js/src/schemas.ts | 56 ++ js/src/tests/client.int.test.ts | 168 +++++- js/src/tests/client.test.ts | 28 + js/src/utils/prompts.ts | 40 ++ python/langsmith/client.py | 43 +- .../tests/integration_tests/test_prompts.py | 1 + 7 files changed, 887 insertions(+), 14 deletions(-) create mode 100644 js/src/utils/prompts.ts diff --git a/js/src/client.ts b/js/src/client.ts index 14746a17a..f867706b1 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -16,6 +16,12 @@ import { FeedbackIngestToken, KVMap, LangChainBaseMessage, + LangSmithSettings, + LikePromptResponse, + ListPromptsResponse, + Prompt, + PromptCommit, + PromptSortField, Run, RunCreate, RunUpdate, @@ -43,6 +49,7 @@ import { import { __version__ } from "./index.js"; import { assertUuid } from "./utils/_uuid.js"; import { warnOnce } from "./utils/warn.js"; +import { isVersionGreaterOrEqual, parsePromptIdentifier } from "./utils/prompts.js"; interface ClientConfig { apiUrl?: string; @@ -418,6 +425,8 @@ export class Client { private fetchOptions: RequestInit; + private settings: LangSmithSettings; + constructor(config: ClientConfig = {}) { const defaultConfig = Client.getDefaultClientConfig(); @@ -746,6 +755,13 @@ export class Client { return true; } + protected async _getSettings() { + if (!this.settings) { + this.settings = await this._get("/settings"); + } + return this.settings; + } + public async createRun(run: CreateRunParams): Promise { if (!this._filterForSampling([run]).length) { return; @@ -2921,4 +2937,553 @@ export class Client { ); return results; } + + protected async _currentTenantIsOwner(owner: string): Promise { + const settings = await this._getSettings(); + return owner == "-" || settings.tenantHandle === owner; + } + + protected async _ownerConflictError( + action: string, owner: string + ): Promise { + const settings = await this._getSettings(); + return new Error( + `Cannot ${action} for another tenant.\n + Current tenant: ${settings.tenantHandle}\n + Requested tenant: ${owner}` + ); + } + + protected async _getLatestCommitHash( + promptOwnerAndName: string, + ): Promise { + const commitsResp = await this.listCommits(promptOwnerAndName, { limit: 1 }); + const commits = commitsResp.commits; + console.log('commits number', commits) + if (commits.length === 0) { + return undefined; + } + return commits[0].commit_hash; + } + + protected async _likeOrUnlikePrompt( + promptIdentifier: string, + like: boolean + ): Promise { + const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); + const response = await this.caller.call( + fetch, + `${this.apiUrl}/likes/${owner}/${promptName}`, + { + method: "POST", + body: JSON.stringify({ like: like }), + headers: { ...this.headers, "Content-Type": "application/json" }, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + if (!response.ok) { + throw new Error( + `Failed to ${like ? "like" : "unlike"} prompt: ${response.status} ${await response.text()}` + ); + } + + return await response.json(); + } + + protected async _getPromptUrl(promptIdentifier: string): Promise { + console.log('print ing promt id', promptIdentifier) + const [owner, promptName, commitHash] = parsePromptIdentifier(promptIdentifier); + if (!(await this._currentTenantIsOwner(owner))) { + if (commitHash !== 'latest') { + return `${this.getHostUrl()}/hub/${owner}/${promptName}/${commitHash.substring(0, 8)}`; + } else { + return `${this.getHostUrl()}/hub/${owner}/${promptName}`; + } + } else { + const settings = await this._getSettings(); + if (commitHash !== 'latest') { + return `${this.getHostUrl()}/prompts/${promptName}/${commitHash.substring(0, 8)}?organizationId=${settings.id}`; + } else { + return `${this.getHostUrl()}/prompts/${promptName}?organizationId=${settings.id}`; + } + } + } + + public async promptExists( + promptIdentifier: string + ): Promise { + const prompt = await this.getPrompt(promptIdentifier); + return !!prompt + } + + public async likePrompt(promptIdentifier: string): Promise { + return this._likeOrUnlikePrompt(promptIdentifier, true); + } + + public async unlikePrompt(promptIdentifier: string): Promise { + return this._likeOrUnlikePrompt(promptIdentifier, false); + } + + public async listCommits( + promptOwnerAndName: string, + options?: { + limit?: number; + offset?: number; + }, + ) { + const { limit = 100, offset = 0 } = options ?? {}; + const res = await this.caller.call( + fetch, + `${this.apiUrl}/commits/${promptOwnerAndName}/?limit=${limit}&offset=${offset}`, + { + method: "GET", + headers: this.headers, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + const json = await res.json(); + if (!res.ok) { + const detail = + typeof json.detail === "string" + ? json.detail + : JSON.stringify(json.detail); + const error = new Error( + `Error ${res.status}: ${res.statusText}\n${detail}`, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any).statusCode = res.status; + throw error; + } + return json; + } + + public async listPrompts( + options?: { + limit?: number, + offset?: number, + isPublic?: boolean, + isArchived?: boolean, + sortField?: PromptSortField, + sortDirection?: 'desc' | 'asc', + query?: string, + } + ): Promise { + const params: Record = { + limit: (options?.limit ?? 100).toString(), + offset: (options?.offset ?? 0).toString(), + sort_field: options?.sortField ?? 'updated_at', + sort_direction: options?.sortDirection ?? 'desc', + is_archived: (!!options?.isArchived).toString(), + }; + + if (options?.isPublic !== undefined) { + params.is_public = options.isPublic.toString(); + } + + if (options?.query) { + params.query = options.query; + } + + const queryString = new URLSearchParams(params).toString(); + + const response = await this.caller.call( + fetch, + `${this.apiUrl}/repos/?${queryString}`, + { + method: "GET", + headers: this.headers, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + const res = await response.json(); + + return { + repos: res.repos.map((result: any) => ({ + owner: result.owner, + repoHandle: result.repo_handle, + description: result.description, + id: result.id, + readme: result.readme, + tenantId: result.tenant_id, + tags: result.tags, + isPublic: result.is_public, + isArchived: result.is_archived, + createdAt: result.created_at, + updatedAt: result.updated_at, + originalRepoId: result.original_repo_id, + upstreamRepoId: result.upstream_repo_id, + fullName: result.full_name, + numLikes: result.num_likes, + numDownloads: result.num_downloads, + numViews: result.num_views, + likedByAuthUser: result.liked_by_auth_user, + lastCommitHash: result.last_commit_hash, + numCommits: result.num_commits, + originalRepoFullName: result.original_repo_full_name, + upstreamRepoFullName: result.upstream_repo_full_name, + })), + total: res.total, + }; + } + + public async getPrompt(promptIdentifier: string): Promise { + const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); + const response = await this.caller.call( + fetch, + `${this.apiUrl}/repos/${owner}/${promptName}`, + { + method: "GET", + headers: this.headers, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + if (response.status === 404) { + return null; + } + + const result = await response.json(); + if (result.repo) { + return { + owner: result.repo.owner, + repoHandle: result.repo.repo_handle, + description: result.repo.description, + id: result.repo.id, + readme: result.repo.readme, + tenantId: result.repo.tenant_id, + tags: result.repo.tags, + isPublic: result.repo.is_public, + isArchived: result.repo.is_archived, + createdAt: result.repo.created_at, + updatedAt: result.repo.updated_at, + originalRepoId: result.repo.original_repo_id, + upstreamRepoId: result.repo.upstream_repo_id, + fullName: result.repo.full_name, + numLikes: result.repo.num_likes, + numDownloads: result.repo.num_downloads, + numViews: result.repo.num_views, + likedByAuthUser: result.repo.liked_by_auth_user, + lastCommitHash: result.repo.last_commit_hash, + numCommits: result.repo.num_commits, + originalRepoFullName: result.repo.original_repo_full_name, + upstreamRepoFullName: result.repo.upstream_repo_full_name, + }; + } else { + return null; + } + } + + public async createPrompt( + promptIdentifier: string, + options?: { + description?: string, + readme?: string, + tags?: string[], + isPublic?: boolean, + } + ): Promise { + const settings = await this._getSettings(); + if (options?.isPublic && !settings.tenantHandle) { + throw new Error( + `Cannot create a public prompt without first\n + creating a LangChain Hub handle. + You can add a handle by creating a public prompt at:\n + https://smith.langchain.com/prompts` + ); + } + + const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); + if (!await this._currentTenantIsOwner(owner)) { + throw await this._ownerConflictError("create a prompt", owner); + } + + const data = { + repo_handle: promptName, + ...(options?.description && { description: options.description }), + ...(options?.readme && { readme: options.readme }), + ...(options?.tags && { tags: options.tags }), + is_public: !!options?.isPublic, + }; + + const response = await this.caller.call( + fetch, + `${this.apiUrl}/repos/`, + { + method: "POST", + headers: { ...this.headers, "Content-Type": "application/json" }, + body: JSON.stringify(data), + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + const { repo } = await response.json(); + console.log('result right here', repo); + return { + owner: repo.owner, + repoHandle: repo.repo_handle, + description: repo.description, + id: repo.id, + readme: repo.readme, + tenantId: repo.tenant_id, + tags: repo.tags, + isPublic: repo.is_public, + isArchived: repo.is_archived, + createdAt: repo.created_at, + updatedAt: repo.updated_at, + originalRepoId: repo.original_repo_id, + upstreamRepoId: repo.upstream_repo_id, + fullName: repo.full_name, + numLikes: repo.num_likes, + numDownloads: repo.num_downloads, + numViews: repo.num_views, + likedByAuthUser: repo.liked_by_auth_user, + lastCommitHash: repo.last_commit_hash, + numCommits: repo.num_commits, + originalRepoFullName: repo.original_repo_full_name, + upstreamRepoFullName: repo.upstream_repo_full_name, + }; + } + + public async createCommit( + promptIdentifier: string, + object: any, + options?: { + parentCommitHash?: string, + } + ): Promise { + if (!await this.promptExists(promptIdentifier)) { + throw new Error("Prompt does not exist, you must create it first."); + } + + const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); + const resolvedParentCommitHash = + (options?.parentCommitHash === "latest" || !options?.parentCommitHash) + ? await this._getLatestCommitHash(`${owner}/${promptName}`) + : options?.parentCommitHash; + + console.log('this is resolved parent commit hash', resolvedParentCommitHash); + + const payload = { + manifest: JSON.parse(JSON.stringify(object)), + parent_commit: resolvedParentCommitHash, + }; + + console.log('latest prompt anyway', await this.listCommits(`${owner}/${promptName}`)); + + const response = await this.caller.call( + fetch, + `${this.apiUrl}/commits/${owner}/${promptName}`, + { + method: "POST", + headers: { ...this.headers, "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + if (!response.ok) { + throw new Error( + `Failed to create commit: ${response.status} ${await response.text()}` + ); + } + + const result = await response.json(); + return this._getPromptUrl(`${owner}/${promptName}${result.commit_hash ? `:${result.commit_hash}` : ''}`); + } + + public async updatePrompt( + promptIdentifier: string, + options?: { + description?: string, + readme?: string, + tags?: string[], + isPublic?: boolean, + isArchived?: boolean, + } + ): Promise> { + if (!await this.promptExists(promptIdentifier)) { + throw new Error("Prompt does not exist, you must create it first."); + } + + const [owner, promptName] = parsePromptIdentifier(promptIdentifier); + + if (!await this._currentTenantIsOwner(owner)) { + throw await this._ownerConflictError("update a prompt", owner); + } + + const payload: Record = {}; + + if (options?.description !== undefined) payload.description = options.description; + if (options?.readme !== undefined) payload.readme = options.readme; + if (options?.tags !== undefined) payload.tags = options.tags; + if (options?.isPublic !== undefined) payload.is_public = options.isPublic; + if (options?.isArchived !== undefined) payload.is_archived = options.isArchived; + + // Check if payload is empty + if (Object.keys(payload).length === 0) { + throw new Error("No valid update options provided"); + } + + const response = await this.caller.call( + fetch, + `${this.apiUrl}/repos/${owner}/${promptName}`, + { + method: "PATCH", + body: JSON.stringify(payload), + headers: { + ...this.headers, + 'Content-Type': 'application/json', + }, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${await response.text()}`); + } + + return response.json(); + } + + public async deletePrompt( + promptIdentifier: string + ): Promise { + if (!await this.promptExists(promptIdentifier)) { + throw new Error("Prompt does not exist, you must create it first."); + } + + const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); + + if (!await this._currentTenantIsOwner(owner)) { + throw await this._ownerConflictError("delete a prompt", owner); + } + + const response = await this.caller.call( + fetch, + `${this.apiUrl}/repos/${owner}/${promptName}`, + { + method: "DELETE", + headers: this.headers, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + return await response.json(); + } + + public async pullPromptCommit( + promptIdentifier: string, + options?: { + includeModel?: boolean + } + ): Promise { + const [owner, promptName, commitHash] = parsePromptIdentifier(promptIdentifier); + console.log('this is current version', this.serverInfo?.version); + const useOptimization = true //isVersionGreaterOrEqual(this.serverInfo?.version, '0.5.23'); + + let passedCommitHash = commitHash; + + if (!useOptimization && commitHash === 'latest') { + const latestCommitHash = await this._getLatestCommitHash(`${owner}/${promptName}`); + if (!latestCommitHash) { + throw new Error('No commits found'); + } else { + passedCommitHash = latestCommitHash; + } + } + + const response = await this.caller.call( + fetch, + `${this.apiUrl}/commits/${owner}/${promptName}/${passedCommitHash}${options?.includeModel ? '?include_model=true' : ''}`, + { + method: "GET", + headers: this.headers, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + if (!response.ok) { + throw new Error( + `Failed to pull prompt commit: ${response.status} ${response.statusText}` + ); + } + + const result = await response.json(); + + return { + owner, + repo: promptName, + commitHash: result.commit_hash, + manifest: result.manifest, + examples: result.examples, + }; + } + + public async pullPrompt( + promptIdentifier: string, + options?: { + includeModel?: boolean, + } + ): Promise { + const promptObject = await this.pullPromptCommit(promptIdentifier, { + includeModel: options?.includeModel + }); + const prompt = JSON.stringify(promptObject.manifest); + // need to add load from lc js + return prompt; + } + + public async pushPrompt( + promptIdentifier: string, + options?: { + object?: any, + parentCommitHash?: string, + isPublic?: boolean, + description?: string, + readme?: string, + tags?: string[], + } + ): Promise { + // Create or update prompt metadata + console.log('prompt exists', await this.promptExists(promptIdentifier)); + if (await this.promptExists(promptIdentifier)) { + await this.updatePrompt(promptIdentifier, { + description: options?.description, + readme: options?.readme, + tags: options?.tags, + isPublic: options?.isPublic, + }); + } else { + await this.createPrompt( + promptIdentifier, + { + description: options?.description, + readme: options?.readme, + tags: options?.tags, + isPublic: options?.isPublic, + } + ); + } + + if (options?.object === null) { + return await this._getPromptUrl(promptIdentifier); + } + + // Create a commit with the new manifest + const url = await this.createCommit(promptIdentifier, options?.object, { + parentCommitHash: options?.parentCommitHash, + }); + return url; + } } diff --git a/js/src/schemas.ts b/js/src/schemas.ts index 0f1ebc126..bbee1f9be 100644 --- a/js/src/schemas.ts +++ b/js/src/schemas.ts @@ -406,3 +406,59 @@ export interface InvocationParamsSchema { ls_max_tokens?: number; ls_stop?: string[]; } + +export interface PromptCommit { + owner: string; + repo: string; + commitHash: string; + manifest: Record; + examples: Array>; +} + +export interface Prompt { + repoHandle: string; + description?: string; + readme?: string; + id: string; + tenantId: string; + createdAt: string; + updatedAt: string; + isPublic: boolean; + isArchived: boolean; + tags: string[]; + originalRepoId?: string; + upstreamRepoId?: string; + owner?: string; + fullName: string; + numLikes: number; + numDownloads: number; + numViews: number; + likedByAuthUser: boolean; + lastCommitHash?: string; + numCommits: number; + originalRepoFullName?: string; + upstreamRepoFullName?: string; +} + +export interface ListPromptsResponse { + repos: Prompt[]; + total: number; +} + +export enum PromptSortField { + NumDownloads = 'num_downloads', + NumViews = 'num_views', + UpdatedAt = 'updated_at', + NumLikes = 'num_likes', +} + +export interface LikePromptResponse { + likes: number; +} + +export interface LangSmithSettings { + id: string; + displayName: string; + createdAt: string; + tenantHandle?: string; +} \ No newline at end of file diff --git a/js/src/tests/client.int.test.ts b/js/src/tests/client.int.test.ts index 29200ce57..8e4f47ff3 100644 --- a/js/src/tests/client.int.test.ts +++ b/js/src/tests/client.int.test.ts @@ -1,5 +1,5 @@ import { Dataset, Run } from "../schemas.js"; -import { FunctionMessage, HumanMessage } from "@langchain/core/messages"; +import { FunctionMessage, HumanMessage, SystemMessage } from "@langchain/core/messages"; import { Client } from "../client.js"; import { v4 as uuidv4 } from "uuid"; @@ -10,6 +10,7 @@ import { toArray, waitUntil, } from "./utils.js"; +import { ChatPromptTemplate } from "@langchain/core/prompts"; type CheckOutputsType = boolean | ((run: Run) => boolean); async function waitUntilRunFound( @@ -748,3 +749,168 @@ test.concurrent("Test run stats", async () => { }); expect(stats).toBeDefined(); }); + +test("Test list prompts", async () => { + const client = new Client(); + const response = await client.listPrompts({ limit: 10, offset: 0 }); + expect(response.repos.length).toBeLessThanOrEqual(10); + expect(response.total).toBeGreaterThanOrEqual(response.repos.length); +}); + +test("Test get prompt", async () => { + const client = new Client(); + const promptName = `test_prompt_${uuidv4().slice(0, 8)}`; + const promptTemplate = ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" }); + + const url = await client.pushPrompt(promptName, { object: promptTemplate }); + expect(url).toBeDefined(); + + const prompt = await client.getPrompt(promptName); + expect(prompt).toBeDefined(); + expect(prompt?.repoHandle).toBe(promptName); + + await client.deletePrompt(promptName); +}); + +test("Test prompt exists", async () => { + const client = new Client(); + const nonExistentPrompt = `non_existent_${uuidv4().slice(0, 8)}`; + expect(await client.promptExists(nonExistentPrompt)).toBe(false); + + const existentPrompt = `existent_${uuidv4().slice(0, 8)}`; + await client.pushPrompt(existentPrompt, { object: ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" })}); + expect(await client.promptExists(existentPrompt)).toBe(true); + + await client.deletePrompt(existentPrompt); +}); + +test("Test update prompt", async () => { + const client = new Client(); + + const promptName = `test_update_prompt_${uuidv4().slice(0, 8)}`; + await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" })}); + + const updatedData = await client.updatePrompt(promptName, { + description: "Updated description", + isPublic: true, + tags: ["test", "update"], + }); + + expect(updatedData).toBeDefined(); + + const updatedPrompt = await client.getPrompt(promptName); + expect(updatedPrompt?.description).toBe("Updated description"); + expect(updatedPrompt?.isPublic).toBe(true); + expect(updatedPrompt?.tags).toEqual(expect.arrayContaining(["test", "update"])); + + await client.deletePrompt(promptName); +}); + +test("Test delete prompt", async () => { + const client = new Client(); + + const promptName = `test_delete_prompt_${uuidv4().slice(0, 8)}`; + await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" })}); + + expect(await client.promptExists(promptName)).toBe(true); + await client.deletePrompt(promptName); + expect(await client.promptExists(promptName)).toBe(false); +}); + +test("Test create commit", async () => { + const client = new Client(); + + const promptName = `test_create_commit_${uuidv4().slice(0, 8)}`; + await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" })}); + + const newTemplate = ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "My question is: {{question}}" }), + ], { templateFormat: "mustache" }); + const commitUrl = await client.createCommit(promptName, newTemplate); + + expect(commitUrl).toBeDefined(); + expect(commitUrl).toContain(promptName); + + await client.deletePrompt(promptName); +}); + +test("Test like and unlike prompt", async () => { + const client = new Client(); + + const promptName = `test_like_prompt_${uuidv4().slice(0, 8)}`; + await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" })}); + + await client.likePrompt(promptName); + let prompt = await client.getPrompt(promptName); + expect(prompt?.numLikes).toBe(1); + + await client.unlikePrompt(promptName); + prompt = await client.getPrompt(promptName); + expect(prompt?.numLikes).toBe(0); + + await client.deletePrompt(promptName); +}); + +test("Test pull prompt commit", async () => { + const client = new Client(); + + const promptName = `test_pull_commit_${uuidv4().slice(0, 8)}`; + const initialTemplate = ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" }); + await client.pushPrompt(promptName, { object: initialTemplate }); + + const promptCommit = await client.pullPromptCommit(promptName); + expect(promptCommit).toBeDefined(); + expect(promptCommit.repo).toBe(promptName); + + await client.deletePrompt(promptName); +}); + +test("Test push and pull prompt", async () => { + const client = new Client(); + + const promptName = `test_push_pull_${uuidv4().slice(0, 8)}`; + const template = ChatPromptTemplate.fromMessages([ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], { templateFormat: "mustache" }); + + await client.pushPrompt(promptName, { + object: template, + description: "Test description", + readme: "Test readme", + tags: ["test", "tag"] + }); + + const pulledPrompt = await client.pullPrompt(promptName); + expect(pulledPrompt).toBeDefined(); + + const promptInfo = await client.getPrompt(promptName); + expect(promptInfo?.description).toBe("Test description"); + expect(promptInfo?.readme).toBe("Test readme"); + expect(promptInfo?.tags).toEqual(expect.arrayContaining(["test", "tag"])); + expect(promptInfo?.isPublic).toBe(false); + + await client.deletePrompt(promptName); +}); diff --git a/js/src/tests/client.test.ts b/js/src/tests/client.test.ts index 000dd460b..381dc734a 100644 --- a/js/src/tests/client.test.ts +++ b/js/src/tests/client.test.ts @@ -6,6 +6,7 @@ import { getLangChainEnvVars, getLangChainEnvVarsMetadata, } from "../utils/env.js"; +import { parsePromptIdentifier } from "../utils/prompts.js"; describe("Client", () => { describe("createLLMExample", () => { @@ -175,4 +176,31 @@ describe("Client", () => { }); }); }); + + describe('parsePromptIdentifier', () => { + it('should parse valid identifiers correctly', () => { + expect(parsePromptIdentifier('name')).toEqual(['-', 'name', 'latest']); + expect(parsePromptIdentifier('owner/name')).toEqual(['owner', 'name', 'latest']); + expect(parsePromptIdentifier('owner/name:commit')).toEqual(['owner', 'name', 'commit']); + expect(parsePromptIdentifier('name:commit')).toEqual(['-', 'name', 'commit']); + }); + + it('should throw an error for invalid identifiers', () => { + const invalidIdentifiers = [ + '', + '/', + ':', + 'owner/', + '/name', + 'owner//name', + 'owner/name/', + 'owner/name/extra', + ':commit', + ]; + + invalidIdentifiers.forEach(identifier => { + expect(() => parsePromptIdentifier(identifier)).toThrowError(`Invalid identifier format: ${identifier}`); + }); + }); + }); }); diff --git a/js/src/utils/prompts.ts b/js/src/utils/prompts.ts new file mode 100644 index 000000000..01f16c29b --- /dev/null +++ b/js/src/utils/prompts.ts @@ -0,0 +1,40 @@ +import { parse as parseVersion } from 'semver'; + +export function isVersionGreaterOrEqual(current_version: string, target_version: string): boolean { + const current = parseVersion(current_version); + const target = parseVersion(target_version); + + if (!current || !target) { + throw new Error('Invalid version format.'); + } + + return current.compare(target) >= 0; +} + +export function parsePromptIdentifier(identifier: string): [string, string, string] { + if ( + !identifier || + identifier.split('/').length > 2 || + identifier.startsWith('/') || + identifier.endsWith('/') || + identifier.split(':').length > 2 + ) { + throw new Error(`Invalid identifier format: ${identifier}`); + } + + const [ownerNamePart, commitPart] = identifier.split(':'); + const commit = commitPart || 'latest'; + + if (ownerNamePart.includes('/')) { + const [owner, name] = ownerNamePart.split('/', 2); + if (!owner || !name) { + throw new Error(`Invalid identifier format: ${identifier}`); + } + return [owner, name, commit]; + } else { + if (!ownerNamePart) { + throw new Error(`Invalid identifier format: ${identifier}`); + } + return ['-', ownerNamePart, commit]; + } +} diff --git a/python/langsmith/client.py b/python/langsmith/client.py index be40dfb02..f23136a1e 100644 --- a/python/langsmith/client.py +++ b/python/langsmith/client.py @@ -4920,13 +4920,22 @@ def _get_prompt_url(self, prompt_identifier: str) -> str: ) if not self._current_tenant_is_owner(owner): - return f"{self._host_url}/hub/{owner}/{prompt_name}:{commit_hash[:8]}" + if commit_hash is not 'latest': + return f"{self._host_url}/hub/{owner}/{prompt_name}:{commit_hash[:8]}" + else: + return f"{self._host_url}/hub/{owner}/{prompt_name}" settings = self._get_settings() - return ( - f"{self._host_url}/prompts/{prompt_name}/{commit_hash[:8]}" - f"?organizationId={settings.id}" - ) + if commit_hash is not 'latest': + return ( + f"{self._host_url}/prompts/{prompt_name}/{commit_hash[:8]}" + f"?organizationId={settings.id}" + ) + else: + return ( + f"{self._host_url}/prompts/{prompt_name}" + f"?organizationId={settings.id}" + ) def _prompt_exists(self, prompt_identifier: str) -> bool: """Check if a prompt exists. @@ -4964,6 +4973,16 @@ def unlike_prompt(self, prompt_identifier: str) -> Dict[str, int]: """ return self._like_or_unlike_prompt(prompt_identifier, like=False) + def list_commits( + prompt_owner_and_name: str, + limit: Optional[int] = 1, + offset: Optional[int] = 0, + ) -> Sequence[ls_schemas.PromptCommit]: + """List commits for a prompt. + """ + + return '' + def list_prompts( self, *, @@ -5110,6 +5129,7 @@ def create_commit( try: from langchain_core.load.dump import dumps + from langchain_core.load.load import loads except ImportError: raise ImportError( "The client.create_commit function requires the langchain_core" @@ -5163,14 +5183,11 @@ def update_prompt( ValueError: If the prompt_identifier is empty. HTTPError: If the server request fails. """ - settings = self._get_settings() - if is_public and not settings.tenant_handle: - raise ValueError( - "Cannot create a public prompt without first\n" - "creating a LangChain Hub handle. " - "You can add a handle by creating a public prompt at:\n" - "https://smith.langchain.com/prompts" - ) + if not self.prompt_exists(prompt_identifier): + raise ls_utils.LangSmithNotFoundError("Prompt does not exist, you must create it first.") + + if not self._current_tenant_is_owner(owner): + raise self._owner_conflict_error("update a prompt", owner) json: Dict[str, Union[str, bool, Sequence[str]]] = {} diff --git a/python/tests/integration_tests/test_prompts.py b/python/tests/integration_tests/test_prompts.py index 80f6e5c4c..607d64fcc 100644 --- a/python/tests/integration_tests/test_prompts.py +++ b/python/tests/integration_tests/test_prompts.py @@ -159,6 +159,7 @@ def test_list_prompts(langsmith_client: Client): response = langsmith_client.list_prompts(limit=10, offset=0) assert isinstance(response, ls_schemas.ListPromptsResponse) assert len(response.repos) <= 10 + assert response.total >= len(response.repos) def test_get_prompt(langsmith_client: Client, prompt_template_1: ChatPromptTemplate): From 064591238b374fa7edda0695eeec44d39bc6d364 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 16:06:12 -0700 Subject: [PATCH 02/16] st --- js/src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index f867706b1..284f8df8f 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -3388,8 +3388,8 @@ export class Client { } ): Promise { const [owner, promptName, commitHash] = parsePromptIdentifier(promptIdentifier); - console.log('this is current version', this.serverInfo?.version); - const useOptimization = true //isVersionGreaterOrEqual(this.serverInfo?.version, '0.5.23'); + const serverInfo = await this._getServerInfo() + const useOptimization = isVersionGreaterOrEqual(serverInfo.version, '0.5.23'); let passedCommitHash = commitHash; From f96aa6d96999b78d64d7569b282b1f42fd55469f Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 16:07:10 -0700 Subject: [PATCH 03/16] version --- js/src/client.ts | 8 -------- js/src/tests/client.int.test.ts | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index 284f8df8f..c8702bdf8 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -2959,7 +2959,6 @@ export class Client { ): Promise { const commitsResp = await this.listCommits(promptOwnerAndName, { limit: 1 }); const commits = commitsResp.commits; - console.log('commits number', commits) if (commits.length === 0) { return undefined; } @@ -2993,7 +2992,6 @@ export class Client { } protected async _getPromptUrl(promptIdentifier: string): Promise { - console.log('print ing promt id', promptIdentifier) const [owner, promptName, commitHash] = parsePromptIdentifier(promptIdentifier); if (!(await this._currentTenantIsOwner(owner))) { if (commitHash !== 'latest') { @@ -3224,7 +3222,6 @@ export class Client { ); const { repo } = await response.json(); - console.log('result right here', repo); return { owner: repo.owner, repoHandle: repo.repo_handle, @@ -3268,15 +3265,11 @@ export class Client { ? await this._getLatestCommitHash(`${owner}/${promptName}`) : options?.parentCommitHash; - console.log('this is resolved parent commit hash', resolvedParentCommitHash); - const payload = { manifest: JSON.parse(JSON.stringify(object)), parent_commit: resolvedParentCommitHash, }; - console.log('latest prompt anyway', await this.listCommits(`${owner}/${promptName}`)); - const response = await this.caller.call( fetch, `${this.apiUrl}/commits/${owner}/${promptName}`, @@ -3456,7 +3449,6 @@ export class Client { } ): Promise { // Create or update prompt metadata - console.log('prompt exists', await this.promptExists(promptIdentifier)); if (await this.promptExists(promptIdentifier)) { await this.updatePrompt(promptIdentifier, { description: options?.description, diff --git a/js/src/tests/client.int.test.ts b/js/src/tests/client.int.test.ts index 8e4f47ff3..41622f9b2 100644 --- a/js/src/tests/client.int.test.ts +++ b/js/src/tests/client.int.test.ts @@ -870,7 +870,7 @@ test("Test like and unlike prompt", async () => { await client.deletePrompt(promptName); }); -test("Test pull prompt commit", async () => { +test.only("Test pull prompt commit", async () => { const client = new Client(); const promptName = `test_pull_commit_${uuidv4().slice(0, 8)}`; From 81e6b4ce660454a987fba5cbb27ec17e36be652e Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 16:10:30 -0700 Subject: [PATCH 04/16] rm python changes --- python/langsmith/client.py | 43 ++++++------------- .../tests/integration_tests/test_prompts.py | 1 - 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/python/langsmith/client.py b/python/langsmith/client.py index f23136a1e..be40dfb02 100644 --- a/python/langsmith/client.py +++ b/python/langsmith/client.py @@ -4920,22 +4920,13 @@ def _get_prompt_url(self, prompt_identifier: str) -> str: ) if not self._current_tenant_is_owner(owner): - if commit_hash is not 'latest': - return f"{self._host_url}/hub/{owner}/{prompt_name}:{commit_hash[:8]}" - else: - return f"{self._host_url}/hub/{owner}/{prompt_name}" + return f"{self._host_url}/hub/{owner}/{prompt_name}:{commit_hash[:8]}" settings = self._get_settings() - if commit_hash is not 'latest': - return ( - f"{self._host_url}/prompts/{prompt_name}/{commit_hash[:8]}" - f"?organizationId={settings.id}" - ) - else: - return ( - f"{self._host_url}/prompts/{prompt_name}" - f"?organizationId={settings.id}" - ) + return ( + f"{self._host_url}/prompts/{prompt_name}/{commit_hash[:8]}" + f"?organizationId={settings.id}" + ) def _prompt_exists(self, prompt_identifier: str) -> bool: """Check if a prompt exists. @@ -4973,16 +4964,6 @@ def unlike_prompt(self, prompt_identifier: str) -> Dict[str, int]: """ return self._like_or_unlike_prompt(prompt_identifier, like=False) - def list_commits( - prompt_owner_and_name: str, - limit: Optional[int] = 1, - offset: Optional[int] = 0, - ) -> Sequence[ls_schemas.PromptCommit]: - """List commits for a prompt. - """ - - return '' - def list_prompts( self, *, @@ -5129,7 +5110,6 @@ def create_commit( try: from langchain_core.load.dump import dumps - from langchain_core.load.load import loads except ImportError: raise ImportError( "The client.create_commit function requires the langchain_core" @@ -5183,11 +5163,14 @@ def update_prompt( ValueError: If the prompt_identifier is empty. HTTPError: If the server request fails. """ - if not self.prompt_exists(prompt_identifier): - raise ls_utils.LangSmithNotFoundError("Prompt does not exist, you must create it first.") - - if not self._current_tenant_is_owner(owner): - raise self._owner_conflict_error("update a prompt", owner) + settings = self._get_settings() + if is_public and not settings.tenant_handle: + raise ValueError( + "Cannot create a public prompt without first\n" + "creating a LangChain Hub handle. " + "You can add a handle by creating a public prompt at:\n" + "https://smith.langchain.com/prompts" + ) json: Dict[str, Union[str, bool, Sequence[str]]] = {} diff --git a/python/tests/integration_tests/test_prompts.py b/python/tests/integration_tests/test_prompts.py index 607d64fcc..80f6e5c4c 100644 --- a/python/tests/integration_tests/test_prompts.py +++ b/python/tests/integration_tests/test_prompts.py @@ -159,7 +159,6 @@ def test_list_prompts(langsmith_client: Client): response = langsmith_client.list_prompts(limit=10, offset=0) assert isinstance(response, ls_schemas.ListPromptsResponse) assert len(response.repos) <= 10 - assert response.total >= len(response.repos) def test_get_prompt(langsmith_client: Client, prompt_template_1: ChatPromptTemplate): From c67b8fcec794f834967dee2ce55e587304253398 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 16:11:44 -0700 Subject: [PATCH 05/16] prettier --- js/src/client.ts | 244 ++++++++++++++++++-------------- js/src/schemas.ts | 12 +- js/src/tests/client.int.test.ts | 125 ++++++++++------ js/src/tests/client.test.ts | 50 ++++--- js/src/utils/prompts.ts | 31 ++-- 5 files changed, 274 insertions(+), 188 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index c8702bdf8..a5f893272 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -49,7 +49,10 @@ import { import { __version__ } from "./index.js"; import { assertUuid } from "./utils/_uuid.js"; import { warnOnce } from "./utils/warn.js"; -import { isVersionGreaterOrEqual, parsePromptIdentifier } from "./utils/prompts.js"; +import { + isVersionGreaterOrEqual, + parsePromptIdentifier, +} from "./utils/prompts.js"; interface ClientConfig { apiUrl?: string; @@ -2944,7 +2947,8 @@ export class Client { } protected async _ownerConflictError( - action: string, owner: string + action: string, + owner: string ): Promise { const settings = await this._getSettings(); return new Error( @@ -2955,9 +2959,11 @@ export class Client { } protected async _getLatestCommitHash( - promptOwnerAndName: string, + promptOwnerAndName: string ): Promise { - const commitsResp = await this.listCommits(promptOwnerAndName, { limit: 1 }); + const commitsResp = await this.listCommits(promptOwnerAndName, { + limit: 1, + }); const commits = commitsResp.commits; if (commits.length === 0) { return undefined; @@ -2984,7 +2990,9 @@ export class Client { if (!response.ok) { throw new Error( - `Failed to ${like ? "like" : "unlike"} prompt: ${response.status} ${await response.text()}` + `Failed to ${like ? "like" : "unlike"} prompt: ${ + response.status + } ${await response.text()}` ); } @@ -2992,35 +3000,46 @@ export class Client { } protected async _getPromptUrl(promptIdentifier: string): Promise { - const [owner, promptName, commitHash] = parsePromptIdentifier(promptIdentifier); + const [owner, promptName, commitHash] = + parsePromptIdentifier(promptIdentifier); if (!(await this._currentTenantIsOwner(owner))) { - if (commitHash !== 'latest') { - return `${this.getHostUrl()}/hub/${owner}/${promptName}/${commitHash.substring(0, 8)}`; + if (commitHash !== "latest") { + return `${this.getHostUrl()}/hub/${owner}/${promptName}/${commitHash.substring( + 0, + 8 + )}`; } else { return `${this.getHostUrl()}/hub/${owner}/${promptName}`; } } else { const settings = await this._getSettings(); - if (commitHash !== 'latest') { - return `${this.getHostUrl()}/prompts/${promptName}/${commitHash.substring(0, 8)}?organizationId=${settings.id}`; + if (commitHash !== "latest") { + return `${this.getHostUrl()}/prompts/${promptName}/${commitHash.substring( + 0, + 8 + )}?organizationId=${settings.id}`; } else { - return `${this.getHostUrl()}/prompts/${promptName}?organizationId=${settings.id}`; + return `${this.getHostUrl()}/prompts/${promptName}?organizationId=${ + settings.id + }`; } } } - public async promptExists( - promptIdentifier: string - ): Promise { + public async promptExists(promptIdentifier: string): Promise { const prompt = await this.getPrompt(promptIdentifier); - return !!prompt + return !!prompt; } - public async likePrompt(promptIdentifier: string): Promise { + public async likePrompt( + promptIdentifier: string + ): Promise { return this._likeOrUnlikePrompt(promptIdentifier, true); } - public async unlikePrompt(promptIdentifier: string): Promise { + public async unlikePrompt( + promptIdentifier: string + ): Promise { return this._likeOrUnlikePrompt(promptIdentifier, false); } @@ -3029,12 +3048,12 @@ export class Client { options?: { limit?: number; offset?: number; - }, + } ) { const { limit = 100, offset = 0 } = options ?? {}; const res = await this.caller.call( fetch, - `${this.apiUrl}/commits/${promptOwnerAndName}/?limit=${limit}&offset=${offset}`, + `${this.apiUrl}/commits/${promptOwnerAndName}/?limit=${limit}&offset=${offset}`, { method: "GET", headers: this.headers, @@ -3049,7 +3068,7 @@ export class Client { ? json.detail : JSON.stringify(json.detail); const error = new Error( - `Error ${res.status}: ${res.statusText}\n${detail}`, + `Error ${res.status}: ${res.statusText}\n${detail}` ); // eslint-disable-next-line @typescript-eslint/no-explicit-any (error as any).statusCode = res.status; @@ -3058,35 +3077,33 @@ export class Client { return json; } - public async listPrompts( - options?: { - limit?: number, - offset?: number, - isPublic?: boolean, - isArchived?: boolean, - sortField?: PromptSortField, - sortDirection?: 'desc' | 'asc', - query?: string, - } - ): Promise { + public async listPrompts(options?: { + limit?: number; + offset?: number; + isPublic?: boolean; + isArchived?: boolean; + sortField?: PromptSortField; + sortDirection?: "desc" | "asc"; + query?: string; + }): Promise { const params: Record = { limit: (options?.limit ?? 100).toString(), offset: (options?.offset ?? 0).toString(), - sort_field: options?.sortField ?? 'updated_at', - sort_direction: options?.sortDirection ?? 'desc', + sort_field: options?.sortField ?? "updated_at", + sort_direction: options?.sortDirection ?? "desc", is_archived: (!!options?.isArchived).toString(), }; - + if (options?.isPublic !== undefined) { params.is_public = options.isPublic.toString(); } - + if (options?.query) { params.query = options.query; } - + const queryString = new URLSearchParams(params).toString(); - + const response = await this.caller.call( fetch, `${this.apiUrl}/repos/?${queryString}`, @@ -3099,7 +3116,7 @@ export class Client { ); const res = await response.json(); - + return { repos: res.repos.map((result: any) => ({ owner: result.owner, @@ -3127,7 +3144,7 @@ export class Client { })), total: res.total, }; - } + } public async getPrompt(promptIdentifier: string): Promise { const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); @@ -3180,10 +3197,10 @@ export class Client { public async createPrompt( promptIdentifier: string, options?: { - description?: string, - readme?: string, - tags?: string[], - isPublic?: boolean, + description?: string; + readme?: string; + tags?: string[]; + isPublic?: boolean; } ): Promise { const settings = await this._getSettings(); @@ -3197,10 +3214,10 @@ export class Client { } const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); - if (!await this._currentTenantIsOwner(owner)) { + if (!(await this._currentTenantIsOwner(owner))) { throw await this._ownerConflictError("create a prompt", owner); } - + const data = { repo_handle: promptName, ...(options?.description && { description: options.description }), @@ -3209,17 +3226,13 @@ export class Client { is_public: !!options?.isPublic, }; - const response = await this.caller.call( - fetch, - `${this.apiUrl}/repos/`, - { - method: "POST", - headers: { ...this.headers, "Content-Type": "application/json" }, - body: JSON.stringify(data), - signal: AbortSignal.timeout(this.timeout_ms), - ...this.fetchOptions, - } - ); + const response = await this.caller.call(fetch, `${this.apiUrl}/repos/`, { + method: "POST", + headers: { ...this.headers, "Content-Type": "application/json" }, + body: JSON.stringify(data), + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + }); const { repo } = await response.json(); return { @@ -3252,16 +3265,16 @@ export class Client { promptIdentifier: string, object: any, options?: { - parentCommitHash?: string, + parentCommitHash?: string; } ): Promise { - if (!await this.promptExists(promptIdentifier)) { + if (!(await this.promptExists(promptIdentifier))) { throw new Error("Prompt does not exist, you must create it first."); } const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); const resolvedParentCommitHash = - (options?.parentCommitHash === "latest" || !options?.parentCommitHash) + options?.parentCommitHash === "latest" || !options?.parentCommitHash ? await this._getLatestCommitHash(`${owner}/${promptName}`) : options?.parentCommitHash; @@ -3289,42 +3302,48 @@ export class Client { } const result = await response.json(); - return this._getPromptUrl(`${owner}/${promptName}${result.commit_hash ? `:${result.commit_hash}` : ''}`); + return this._getPromptUrl( + `${owner}/${promptName}${ + result.commit_hash ? `:${result.commit_hash}` : "" + }` + ); } public async updatePrompt( promptIdentifier: string, options?: { - description?: string, - readme?: string, - tags?: string[], - isPublic?: boolean, - isArchived?: boolean, + description?: string; + readme?: string; + tags?: string[]; + isPublic?: boolean; + isArchived?: boolean; } ): Promise> { - if (!await this.promptExists(promptIdentifier)) { + if (!(await this.promptExists(promptIdentifier))) { throw new Error("Prompt does not exist, you must create it first."); } - + const [owner, promptName] = parsePromptIdentifier(promptIdentifier); - - if (!await this._currentTenantIsOwner(owner)) { + + if (!(await this._currentTenantIsOwner(owner))) { throw await this._ownerConflictError("update a prompt", owner); } - + const payload: Record = {}; - - if (options?.description !== undefined) payload.description = options.description; + + if (options?.description !== undefined) + payload.description = options.description; if (options?.readme !== undefined) payload.readme = options.readme; if (options?.tags !== undefined) payload.tags = options.tags; if (options?.isPublic !== undefined) payload.is_public = options.isPublic; - if (options?.isArchived !== undefined) payload.is_archived = options.isArchived; - + if (options?.isArchived !== undefined) + payload.is_archived = options.isArchived; + // Check if payload is empty if (Object.keys(payload).length === 0) { throw new Error("No valid update options provided"); } - + const response = await this.caller.call( fetch, `${this.apiUrl}/repos/${owner}/${promptName}`, @@ -3333,30 +3352,30 @@ export class Client { body: JSON.stringify(payload), headers: { ...this.headers, - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, } ); - + if (!response.ok) { - throw new Error(`HTTP Error: ${response.status} - ${await response.text()}`); + throw new Error( + `HTTP Error: ${response.status} - ${await response.text()}` + ); } - + return response.json(); } - public async deletePrompt( - promptIdentifier: string - ): Promise { - if (!await this.promptExists(promptIdentifier)) { + public async deletePrompt(promptIdentifier: string): Promise { + if (!(await this.promptExists(promptIdentifier))) { throw new Error("Prompt does not exist, you must create it first."); } const [owner, promptName, _] = parsePromptIdentifier(promptIdentifier); - if (!await this._currentTenantIsOwner(owner)) { + if (!(await this._currentTenantIsOwner(owner))) { throw await this._ownerConflictError("delete a prompt", owner); } @@ -3377,19 +3396,25 @@ export class Client { public async pullPromptCommit( promptIdentifier: string, options?: { - includeModel?: boolean + includeModel?: boolean; } ): Promise { - const [owner, promptName, commitHash] = parsePromptIdentifier(promptIdentifier); - const serverInfo = await this._getServerInfo() - const useOptimization = isVersionGreaterOrEqual(serverInfo.version, '0.5.23'); + const [owner, promptName, commitHash] = + parsePromptIdentifier(promptIdentifier); + const serverInfo = await this._getServerInfo(); + const useOptimization = isVersionGreaterOrEqual( + serverInfo.version, + "0.5.23" + ); let passedCommitHash = commitHash; - if (!useOptimization && commitHash === 'latest') { - const latestCommitHash = await this._getLatestCommitHash(`${owner}/${promptName}`); + if (!useOptimization && commitHash === "latest") { + const latestCommitHash = await this._getLatestCommitHash( + `${owner}/${promptName}` + ); if (!latestCommitHash) { - throw new Error('No commits found'); + throw new Error("No commits found"); } else { passedCommitHash = latestCommitHash; } @@ -3397,7 +3422,9 @@ export class Client { const response = await this.caller.call( fetch, - `${this.apiUrl}/commits/${owner}/${promptName}/${passedCommitHash}${options?.includeModel ? '?include_model=true' : ''}`, + `${this.apiUrl}/commits/${owner}/${promptName}/${passedCommitHash}${ + options?.includeModel ? "?include_model=true" : "" + }`, { method: "GET", headers: this.headers, @@ -3422,15 +3449,15 @@ export class Client { examples: result.examples, }; } - + public async pullPrompt( promptIdentifier: string, options?: { - includeModel?: boolean, + includeModel?: boolean; } ): Promise { const promptObject = await this.pullPromptCommit(promptIdentifier, { - includeModel: options?.includeModel + includeModel: options?.includeModel, }); const prompt = JSON.stringify(promptObject.manifest); // need to add load from lc js @@ -3440,12 +3467,12 @@ export class Client { public async pushPrompt( promptIdentifier: string, options?: { - object?: any, - parentCommitHash?: string, - isPublic?: boolean, - description?: string, - readme?: string, - tags?: string[], + object?: any; + parentCommitHash?: string; + isPublic?: boolean; + description?: string; + readme?: string; + tags?: string[]; } ): Promise { // Create or update prompt metadata @@ -3457,15 +3484,12 @@ export class Client { isPublic: options?.isPublic, }); } else { - await this.createPrompt( - promptIdentifier, - { - description: options?.description, - readme: options?.readme, - tags: options?.tags, - isPublic: options?.isPublic, - } - ); + await this.createPrompt(promptIdentifier, { + description: options?.description, + readme: options?.readme, + tags: options?.tags, + isPublic: options?.isPublic, + }); } if (options?.object === null) { diff --git a/js/src/schemas.ts b/js/src/schemas.ts index bbee1f9be..0e8a0bc7c 100644 --- a/js/src/schemas.ts +++ b/js/src/schemas.ts @@ -446,10 +446,10 @@ export interface ListPromptsResponse { } export enum PromptSortField { - NumDownloads = 'num_downloads', - NumViews = 'num_views', - UpdatedAt = 'updated_at', - NumLikes = 'num_likes', + NumDownloads = "num_downloads", + NumViews = "num_views", + UpdatedAt = "updated_at", + NumLikes = "num_likes", } export interface LikePromptResponse { @@ -460,5 +460,5 @@ export interface LangSmithSettings { id: string; displayName: string; createdAt: string; - tenantHandle?: string; -} \ No newline at end of file + tenantHandle?: string; +} diff --git a/js/src/tests/client.int.test.ts b/js/src/tests/client.int.test.ts index 41622f9b2..4d16fc347 100644 --- a/js/src/tests/client.int.test.ts +++ b/js/src/tests/client.int.test.ts @@ -1,5 +1,9 @@ import { Dataset, Run } from "../schemas.js"; -import { FunctionMessage, HumanMessage, SystemMessage } from "@langchain/core/messages"; +import { + FunctionMessage, + HumanMessage, + SystemMessage, +} from "@langchain/core/messages"; import { Client } from "../client.js"; import { v4 as uuidv4 } from "uuid"; @@ -760,11 +764,14 @@ test("Test list prompts", async () => { test("Test get prompt", async () => { const client = new Client(); const promptName = `test_prompt_${uuidv4().slice(0, 8)}`; - const promptTemplate = ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" }); - + const promptTemplate = ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ); + const url = await client.pushPrompt(promptName, { object: promptTemplate }); expect(url).toBeDefined(); @@ -781,10 +788,15 @@ test("Test prompt exists", async () => { expect(await client.promptExists(nonExistentPrompt)).toBe(false); const existentPrompt = `existent_${uuidv4().slice(0, 8)}`; - await client.pushPrompt(existentPrompt, { object: ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" })}); + await client.pushPrompt(existentPrompt, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); expect(await client.promptExists(existentPrompt)).toBe(true); await client.deletePrompt(existentPrompt); @@ -794,10 +806,15 @@ test("Test update prompt", async () => { const client = new Client(); const promptName = `test_update_prompt_${uuidv4().slice(0, 8)}`; - await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" })}); + await client.pushPrompt(promptName, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); const updatedData = await client.updatePrompt(promptName, { description: "Updated description", @@ -810,7 +827,9 @@ test("Test update prompt", async () => { const updatedPrompt = await client.getPrompt(promptName); expect(updatedPrompt?.description).toBe("Updated description"); expect(updatedPrompt?.isPublic).toBe(true); - expect(updatedPrompt?.tags).toEqual(expect.arrayContaining(["test", "update"])); + expect(updatedPrompt?.tags).toEqual( + expect.arrayContaining(["test", "update"]) + ); await client.deletePrompt(promptName); }); @@ -819,10 +838,15 @@ test("Test delete prompt", async () => { const client = new Client(); const promptName = `test_delete_prompt_${uuidv4().slice(0, 8)}`; - await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" })}); + await client.pushPrompt(promptName, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); expect(await client.promptExists(promptName)).toBe(true); await client.deletePrompt(promptName); @@ -833,15 +857,23 @@ test("Test create commit", async () => { const client = new Client(); const promptName = `test_create_commit_${uuidv4().slice(0, 8)}`; - await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" })}); - - const newTemplate = ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "My question is: {{question}}" }), - ], { templateFormat: "mustache" }); + await client.pushPrompt(promptName, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); + + const newTemplate = ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "My question is: {{question}}" }), + ], + { templateFormat: "mustache" } + ); const commitUrl = await client.createCommit(promptName, newTemplate); expect(commitUrl).toBeDefined(); @@ -854,10 +886,15 @@ test("Test like and unlike prompt", async () => { const client = new Client(); const promptName = `test_like_prompt_${uuidv4().slice(0, 8)}`; - await client.pushPrompt(promptName, { object: ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" })}); + await client.pushPrompt(promptName, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); await client.likePrompt(promptName); let prompt = await client.getPrompt(promptName); @@ -874,10 +911,13 @@ test.only("Test pull prompt commit", async () => { const client = new Client(); const promptName = `test_pull_commit_${uuidv4().slice(0, 8)}`; - const initialTemplate = ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" }); + const initialTemplate = ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ); await client.pushPrompt(promptName, { object: initialTemplate }); const promptCommit = await client.pullPromptCommit(promptName); @@ -891,16 +931,19 @@ test("Test push and pull prompt", async () => { const client = new Client(); const promptName = `test_push_pull_${uuidv4().slice(0, 8)}`; - const template = ChatPromptTemplate.fromMessages([ - new SystemMessage({ content: "System message" }), - new HumanMessage({ content: "{{question}}" }), - ], { templateFormat: "mustache" }); + const template = ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ); await client.pushPrompt(promptName, { object: template, description: "Test description", readme: "Test readme", - tags: ["test", "tag"] + tags: ["test", "tag"], }); const pulledPrompt = await client.pullPrompt(promptName); diff --git a/js/src/tests/client.test.ts b/js/src/tests/client.test.ts index 381dc734a..54a8e68a7 100644 --- a/js/src/tests/client.test.ts +++ b/js/src/tests/client.test.ts @@ -177,29 +177,43 @@ describe("Client", () => { }); }); - describe('parsePromptIdentifier', () => { - it('should parse valid identifiers correctly', () => { - expect(parsePromptIdentifier('name')).toEqual(['-', 'name', 'latest']); - expect(parsePromptIdentifier('owner/name')).toEqual(['owner', 'name', 'latest']); - expect(parsePromptIdentifier('owner/name:commit')).toEqual(['owner', 'name', 'commit']); - expect(parsePromptIdentifier('name:commit')).toEqual(['-', 'name', 'commit']); + describe("parsePromptIdentifier", () => { + it("should parse valid identifiers correctly", () => { + expect(parsePromptIdentifier("name")).toEqual(["-", "name", "latest"]); + expect(parsePromptIdentifier("owner/name")).toEqual([ + "owner", + "name", + "latest", + ]); + expect(parsePromptIdentifier("owner/name:commit")).toEqual([ + "owner", + "name", + "commit", + ]); + expect(parsePromptIdentifier("name:commit")).toEqual([ + "-", + "name", + "commit", + ]); }); - it('should throw an error for invalid identifiers', () => { + it("should throw an error for invalid identifiers", () => { const invalidIdentifiers = [ - '', - '/', - ':', - 'owner/', - '/name', - 'owner//name', - 'owner/name/', - 'owner/name/extra', - ':commit', + "", + "/", + ":", + "owner/", + "/name", + "owner//name", + "owner/name/", + "owner/name/extra", + ":commit", ]; - invalidIdentifiers.forEach(identifier => { - expect(() => parsePromptIdentifier(identifier)).toThrowError(`Invalid identifier format: ${identifier}`); + invalidIdentifiers.forEach((identifier) => { + expect(() => parsePromptIdentifier(identifier)).toThrowError( + `Invalid identifier format: ${identifier}` + ); }); }); }); diff --git a/js/src/utils/prompts.ts b/js/src/utils/prompts.ts index 01f16c29b..53bbee3c4 100644 --- a/js/src/utils/prompts.ts +++ b/js/src/utils/prompts.ts @@ -1,32 +1,37 @@ -import { parse as parseVersion } from 'semver'; +import { parse as parseVersion } from "semver"; -export function isVersionGreaterOrEqual(current_version: string, target_version: string): boolean { +export function isVersionGreaterOrEqual( + current_version: string, + target_version: string +): boolean { const current = parseVersion(current_version); const target = parseVersion(target_version); if (!current || !target) { - throw new Error('Invalid version format.'); + throw new Error("Invalid version format."); } return current.compare(target) >= 0; } -export function parsePromptIdentifier(identifier: string): [string, string, string] { +export function parsePromptIdentifier( + identifier: string +): [string, string, string] { if ( !identifier || - identifier.split('/').length > 2 || - identifier.startsWith('/') || - identifier.endsWith('/') || - identifier.split(':').length > 2 + identifier.split("/").length > 2 || + identifier.startsWith("/") || + identifier.endsWith("/") || + identifier.split(":").length > 2 ) { throw new Error(`Invalid identifier format: ${identifier}`); } - const [ownerNamePart, commitPart] = identifier.split(':'); - const commit = commitPart || 'latest'; + const [ownerNamePart, commitPart] = identifier.split(":"); + const commit = commitPart || "latest"; - if (ownerNamePart.includes('/')) { - const [owner, name] = ownerNamePart.split('/', 2); + if (ownerNamePart.includes("/")) { + const [owner, name] = ownerNamePart.split("/", 2); if (!owner || !name) { throw new Error(`Invalid identifier format: ${identifier}`); } @@ -35,6 +40,6 @@ export function parsePromptIdentifier(identifier: string): [string, string, stri if (!ownerNamePart) { throw new Error(`Invalid identifier format: ${identifier}`); } - return ['-', ownerNamePart, commit]; + return ["-", ownerNamePart, commit]; } } From c15ccca44d396cbd1c3f2b6ce8083669d2539120 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 16:13:33 -0700 Subject: [PATCH 06/16] add semver --- js/package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/js/package.json b/js/package.json index 90e3086bb..a81a87b5d 100644 --- a/js/package.json +++ b/js/package.json @@ -97,13 +97,13 @@ "commander": "^10.0.1", "p-queue": "^6.6.2", "p-retry": "4", + "semver": "^7.6.3", "uuid": "^9.0.0" }, "devDependencies": { "@babel/preset-env": "^7.22.4", "@faker-js/faker": "^8.4.1", "@jest/globals": "^29.5.0", - "langchain": "^0.2.0", "@langchain/core": "^0.2.0", "@langchain/langgraph": "^0.0.19", "@tsconfig/recommended": "^1.0.2", @@ -119,6 +119,7 @@ "eslint-plugin-no-instanceof": "^1.0.1", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.5.0", + "langchain": "^0.2.0", "openai": "^4.38.5", "prettier": "^2.8.8", "ts-jest": "^29.1.0", @@ -126,9 +127,9 @@ "typescript": "^5.4.5" }, "peerDependencies": { - "openai": "*", + "@langchain/core": "*", "langchain": "*", - "@langchain/core": "*" + "openai": "*" }, "peerDependenciesMeta": { "openai": { @@ -261,4 +262,4 @@ }, "./package.json": "./package.json" } -} \ No newline at end of file +} From c70acb15d2142182fc8cc3d03723393dd4598b0f Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Tue, 23 Jul 2024 16:24:14 -0700 Subject: [PATCH 07/16] add unit test --- js/src/tests/client.test.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/js/src/tests/client.test.ts b/js/src/tests/client.test.ts index 54a8e68a7..694fb1e3c 100644 --- a/js/src/tests/client.test.ts +++ b/js/src/tests/client.test.ts @@ -6,7 +6,10 @@ import { getLangChainEnvVars, getLangChainEnvVarsMetadata, } from "../utils/env.js"; -import { parsePromptIdentifier } from "../utils/prompts.js"; +import { + isVersionGreaterOrEqual, + parsePromptIdentifier, +} from "../utils/prompts.js"; describe("Client", () => { describe("createLLMExample", () => { @@ -177,6 +180,23 @@ describe("Client", () => { }); }); + describe("isVersionGreaterOrEqual", () => { + it("should return true if the version is greater or equal", () => { + // Test versions equal to 0.5.23 + expect(isVersionGreaterOrEqual("0.5.23", "0.5.23")).toBe(true); + + // Test versions greater than 0.5.23 + expect(isVersionGreaterOrEqual("0.5.24", "0.5.23")); + expect(isVersionGreaterOrEqual("0.6.0", "0.5.23")); + expect(isVersionGreaterOrEqual("1.0.0", "0.5.23")); + + // Test versions less than 0.5.23 + expect(isVersionGreaterOrEqual("0.5.22", "0.5.23")).toBe(false); + expect(isVersionGreaterOrEqual("0.5.0", "0.5.23")).toBe(false); + expect(isVersionGreaterOrEqual("0.4.99", "0.5.23")).toBe(false); + }); + }); + describe("parsePromptIdentifier", () => { it("should parse valid identifiers correctly", () => { expect(parsePromptIdentifier("name")).toEqual(["-", "name", "latest"]); From 9a305a2b8ca9fc737cbad14f532c5bdf6a2b3acf Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Thu, 25 Jul 2024 16:36:27 -0700 Subject: [PATCH 08/16] store settings as promise --- js/src/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index a5f893272..aede56745 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -428,7 +428,7 @@ export class Client { private fetchOptions: RequestInit; - private settings: LangSmithSettings; + private settings: Promise | null; constructor(config: ClientConfig = {}) { const defaultConfig = Client.getDefaultClientConfig(); @@ -760,9 +760,10 @@ export class Client { protected async _getSettings() { if (!this.settings) { - this.settings = await this._get("/settings"); + this.settings = this._get("/settings"); } - return this.settings; + + return await this.settings; } public async createRun(run: CreateRunParams): Promise { @@ -3103,7 +3104,6 @@ export class Client { } const queryString = new URLSearchParams(params).toString(); - const response = await this.caller.call( fetch, `${this.apiUrl}/repos/?${queryString}`, From eb8a79f0c2eab20161a862c6ecd70466a3812ca7 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Thu, 25 Jul 2024 17:17:58 -0700 Subject: [PATCH 09/16] fixes --- js/src/client.ts | 251 ++++++++++++++------------------ js/src/schemas.ts | 47 +++--- js/src/tests/client.int.test.ts | 21 +-- 3 files changed, 144 insertions(+), 175 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index aede56745..3914a2a7f 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -18,6 +18,7 @@ import { LangChainBaseMessage, LangSmithSettings, LikePromptResponse, + ListCommitsResponse, ListPromptsResponse, Prompt, PromptCommit, @@ -581,9 +582,10 @@ export class Client { const response = await this._getResponse(path, queryParams); return response.json() as T; } - private async *_getPaginated( + private async *_getPaginated( path: string, - queryParams: URLSearchParams = new URLSearchParams() + queryParams: URLSearchParams = new URLSearchParams(), + transform?: (data: TResponse) => T[] ): AsyncIterable { let offset = Number(queryParams.get("offset")) || 0; const limit = Number(queryParams.get("limit")) || 100; @@ -603,7 +605,8 @@ export class Client { `Failed to fetch ${path}: ${response.status} ${response.statusText}` ); } - const items: T[] = await response.json(); + + const items: T[] =transform ? transform(await response.json()) : await response.json(); if (items.length === 0) { break; @@ -2944,7 +2947,7 @@ export class Client { protected async _currentTenantIsOwner(owner: string): Promise { const settings = await this._getSettings(); - return owner == "-" || settings.tenantHandle === owner; + return owner == "-" || settings.tenant_handle === owner; } protected async _ownerConflictError( @@ -2954,7 +2957,7 @@ export class Client { const settings = await this._getSettings(); return new Error( `Cannot ${action} for another tenant.\n - Current tenant: ${settings.tenantHandle}\n + Current tenant: ${settings.tenant_handle}\n Requested tenant: ${owner}` ); } @@ -2962,14 +2965,36 @@ export class Client { protected async _getLatestCommitHash( promptOwnerAndName: string ): Promise { - const commitsResp = await this.listCommits(promptOwnerAndName, { - limit: 1, - }); - const commits = commitsResp.commits; - if (commits.length === 0) { + const res = await this.caller.call( + fetch, + `${this.apiUrl}/commits/${promptOwnerAndName}/?limit=${1}&offset=${0}`, + { + method: "GET", + headers: this.headers, + signal: AbortSignal.timeout(this.timeout_ms), + ...this.fetchOptions, + } + ); + + const json = await res.json(); + if (!res.ok) { + const detail = + typeof json.detail === "string" + ? json.detail + : JSON.stringify(json.detail); + const error = new Error( + `Error ${res.status}: ${res.statusText}\n${detail}` + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any).statusCode = res.status; + throw error; + } + + if (json.commits.length === 0) { return undefined; } - return commits[0].commit_hash; + + return json.commits[0].commit_hash; } protected async _likeOrUnlikePrompt( @@ -3044,106 +3069,89 @@ export class Client { return this._likeOrUnlikePrompt(promptIdentifier, false); } - public async listCommits( - promptOwnerAndName: string, - options?: { - limit?: number; - offset?: number; + public async *listCommits(promptOwnerAndName: string): AsyncIterableIterator { + for await (const commits of this._getPaginated( + `/commits/${promptOwnerAndName}/`, + {} as URLSearchParams, + (res) => res.commits, + )) { + yield* commits; } - ) { - const { limit = 100, offset = 0 } = options ?? {}; - const res = await this.caller.call( - fetch, - `${this.apiUrl}/commits/${promptOwnerAndName}/?limit=${limit}&offset=${offset}`, - { - method: "GET", - headers: this.headers, - signal: AbortSignal.timeout(this.timeout_ms), - ...this.fetchOptions, + } + + public async *listProjects2({ + projectIds, + name, + nameContains, + referenceDatasetId, + referenceDatasetName, + referenceFree, + }: { + projectIds?: string[]; + name?: string; + nameContains?: string; + referenceDatasetId?: string; + referenceDatasetName?: string; + referenceFree?: boolean; + } = {}): AsyncIterable { + const params = new URLSearchParams(); + if (projectIds !== undefined) { + for (const projectId of projectIds) { + params.append("id", projectId); } - ); - const json = await res.json(); - if (!res.ok) { - const detail = - typeof json.detail === "string" - ? json.detail - : JSON.stringify(json.detail); - const error = new Error( - `Error ${res.status}: ${res.statusText}\n${detail}` - ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error as any).statusCode = res.status; - throw error; } - return json; + if (name !== undefined) { + params.append("name", name); + } + if (nameContains !== undefined) { + params.append("name_contains", nameContains); + } + if (referenceDatasetId !== undefined) { + params.append("reference_dataset", referenceDatasetId); + } else if (referenceDatasetName !== undefined) { + const dataset = await this.readDataset({ + datasetName: referenceDatasetName, + }); + params.append("reference_dataset", dataset.id); + } + if (referenceFree !== undefined) { + params.append("reference_free", referenceFree.toString()); + } + for await (const projects of this._getPaginated( + "/sessions", + params + )) { + yield* projects; + } } - public async listPrompts(options?: { - limit?: number; - offset?: number; + public async *listPrompts(options?: { isPublic?: boolean; isArchived?: boolean; sortField?: PromptSortField; sortDirection?: "desc" | "asc"; query?: string; - }): Promise { - const params: Record = { - limit: (options?.limit ?? 100).toString(), - offset: (options?.offset ?? 0).toString(), - sort_field: options?.sortField ?? "updated_at", - sort_direction: options?.sortDirection ?? "desc", - is_archived: (!!options?.isArchived).toString(), - }; + }): AsyncIterableIterator { + const params = new URLSearchParams(); + params.append("sort_field", options?.sortField ?? "updated_at"); + params.append("sort_direction", options?.sortDirection ?? "desc"); + params.append("is_archived", (!!options?.isArchived).toString()); if (options?.isPublic !== undefined) { - params.is_public = options.isPublic.toString(); + params.append("is_public", options.isPublic.toString()); } if (options?.query) { - params.query = options.query; + params.append("query", options.query); } - const queryString = new URLSearchParams(params).toString(); - const response = await this.caller.call( - fetch, - `${this.apiUrl}/repos/?${queryString}`, - { - method: "GET", - headers: this.headers, - signal: AbortSignal.timeout(this.timeout_ms), - ...this.fetchOptions, - } - ); - - const res = await response.json(); - - return { - repos: res.repos.map((result: any) => ({ - owner: result.owner, - repoHandle: result.repo_handle, - description: result.description, - id: result.id, - readme: result.readme, - tenantId: result.tenant_id, - tags: result.tags, - isPublic: result.is_public, - isArchived: result.is_archived, - createdAt: result.created_at, - updatedAt: result.updated_at, - originalRepoId: result.original_repo_id, - upstreamRepoId: result.upstream_repo_id, - fullName: result.full_name, - numLikes: result.num_likes, - numDownloads: result.num_downloads, - numViews: result.num_views, - likedByAuthUser: result.liked_by_auth_user, - lastCommitHash: result.last_commit_hash, - numCommits: result.num_commits, - originalRepoFullName: result.original_repo_full_name, - upstreamRepoFullName: result.upstream_repo_full_name, - })), - total: res.total, - }; + for await (const prompts of this._getPaginated( + "/repos", + params, + (res) => res.repos, + )) { + yield* prompts; + } } public async getPrompt(promptIdentifier: string): Promise { @@ -3165,30 +3173,7 @@ export class Client { const result = await response.json(); if (result.repo) { - return { - owner: result.repo.owner, - repoHandle: result.repo.repo_handle, - description: result.repo.description, - id: result.repo.id, - readme: result.repo.readme, - tenantId: result.repo.tenant_id, - tags: result.repo.tags, - isPublic: result.repo.is_public, - isArchived: result.repo.is_archived, - createdAt: result.repo.created_at, - updatedAt: result.repo.updated_at, - originalRepoId: result.repo.original_repo_id, - upstreamRepoId: result.repo.upstream_repo_id, - fullName: result.repo.full_name, - numLikes: result.repo.num_likes, - numDownloads: result.repo.num_downloads, - numViews: result.repo.num_views, - likedByAuthUser: result.repo.liked_by_auth_user, - lastCommitHash: result.repo.last_commit_hash, - numCommits: result.repo.num_commits, - originalRepoFullName: result.repo.original_repo_full_name, - upstreamRepoFullName: result.repo.upstream_repo_full_name, - }; + return result.repo as Prompt; } else { return null; } @@ -3204,7 +3189,7 @@ export class Client { } ): Promise { const settings = await this._getSettings(); - if (options?.isPublic && !settings.tenantHandle) { + if (options?.isPublic && !settings.tenant_handle) { throw new Error( `Cannot create a public prompt without first\n creating a LangChain Hub handle. @@ -3235,30 +3220,7 @@ export class Client { }); const { repo } = await response.json(); - return { - owner: repo.owner, - repoHandle: repo.repo_handle, - description: repo.description, - id: repo.id, - readme: repo.readme, - tenantId: repo.tenant_id, - tags: repo.tags, - isPublic: repo.is_public, - isArchived: repo.is_archived, - createdAt: repo.created_at, - updatedAt: repo.updated_at, - originalRepoId: repo.original_repo_id, - upstreamRepoId: repo.upstream_repo_id, - fullName: repo.full_name, - numLikes: repo.num_likes, - numDownloads: repo.num_downloads, - numViews: repo.num_views, - likedByAuthUser: repo.liked_by_auth_user, - lastCommitHash: repo.last_commit_hash, - numCommits: repo.num_commits, - originalRepoFullName: repo.original_repo_full_name, - upstreamRepoFullName: repo.upstream_repo_full_name, - }; + return repo as Prompt; } public async createCommit( @@ -3444,7 +3406,7 @@ export class Client { return { owner, repo: promptName, - commitHash: result.commit_hash, + commit_hash: result.commit_hash, manifest: result.manifest, examples: result.examples, }; @@ -3460,7 +3422,6 @@ export class Client { includeModel: options?.includeModel, }); const prompt = JSON.stringify(promptObject.manifest); - // need to add load from lc js return prompt; } diff --git a/js/src/schemas.ts b/js/src/schemas.ts index 0e8a0bc7c..350f114ba 100644 --- a/js/src/schemas.ts +++ b/js/src/schemas.ts @@ -410,34 +410,34 @@ export interface InvocationParamsSchema { export interface PromptCommit { owner: string; repo: string; - commitHash: string; + commit_hash: string; manifest: Record; examples: Array>; } export interface Prompt { - repoHandle: string; + repo_handle: string; description?: string; readme?: string; id: string; - tenantId: string; - createdAt: string; - updatedAt: string; - isPublic: boolean; - isArchived: boolean; + tenant_id: string; + created_at: string; + updated_at: string; + is_public: boolean; + is_archived: boolean; tags: string[]; - originalRepoId?: string; - upstreamRepoId?: string; + original_repo_id?: string; + upstream_repo_id?: string; owner?: string; - fullName: string; - numLikes: number; - numDownloads: number; - numViews: number; - likedByAuthUser: boolean; - lastCommitHash?: string; - numCommits: number; - originalRepoFullName?: string; - upstreamRepoFullName?: string; + full_name: string; + num_likes: number; + num_downloads: number; + num_views: number; + liked_by_auth_user: boolean; + last_commit_hash?: string; + num_commits: number; + original_repo_full_name?: string; + upstream_repo_full_name?: string; } export interface ListPromptsResponse { @@ -445,6 +445,11 @@ export interface ListPromptsResponse { total: number; } +export interface ListCommitsResponse { + commits: PromptCommit[]; + total: number; +} + export enum PromptSortField { NumDownloads = "num_downloads", NumViews = "num_views", @@ -458,7 +463,7 @@ export interface LikePromptResponse { export interface LangSmithSettings { id: string; - displayName: string; - createdAt: string; - tenantHandle?: string; + display_name: string; + created_at: string; + tenant_handle?: string; } diff --git a/js/src/tests/client.int.test.ts b/js/src/tests/client.int.test.ts index 4d16fc347..824e47125 100644 --- a/js/src/tests/client.int.test.ts +++ b/js/src/tests/client.int.test.ts @@ -756,9 +756,12 @@ test.concurrent("Test run stats", async () => { test("Test list prompts", async () => { const client = new Client(); - const response = await client.listPrompts({ limit: 10, offset: 0 }); - expect(response.repos.length).toBeLessThanOrEqual(10); - expect(response.total).toBeGreaterThanOrEqual(response.repos.length); + const response = await client.listPrompts({ isPublic: true }); + expect(response).toBeDefined(); + for await (const prompt of response) { + console.log("this is what prompt looks like", prompt); + expect(prompt).toBeDefined(); + } }); test("Test get prompt", async () => { @@ -777,7 +780,7 @@ test("Test get prompt", async () => { const prompt = await client.getPrompt(promptName); expect(prompt).toBeDefined(); - expect(prompt?.repoHandle).toBe(promptName); + expect(prompt?.repo_handle).toBe(promptName); await client.deletePrompt(promptName); }); @@ -826,7 +829,7 @@ test("Test update prompt", async () => { const updatedPrompt = await client.getPrompt(promptName); expect(updatedPrompt?.description).toBe("Updated description"); - expect(updatedPrompt?.isPublic).toBe(true); + expect(updatedPrompt?.is_public).toBe(true); expect(updatedPrompt?.tags).toEqual( expect.arrayContaining(["test", "update"]) ); @@ -898,16 +901,16 @@ test("Test like and unlike prompt", async () => { await client.likePrompt(promptName); let prompt = await client.getPrompt(promptName); - expect(prompt?.numLikes).toBe(1); + expect(prompt?.num_likes).toBe(1); await client.unlikePrompt(promptName); prompt = await client.getPrompt(promptName); - expect(prompt?.numLikes).toBe(0); + expect(prompt?.num_likes).toBe(0); await client.deletePrompt(promptName); }); -test.only("Test pull prompt commit", async () => { +test("Test pull prompt commit", async () => { const client = new Client(); const promptName = `test_pull_commit_${uuidv4().slice(0, 8)}`; @@ -953,7 +956,7 @@ test("Test push and pull prompt", async () => { expect(promptInfo?.description).toBe("Test description"); expect(promptInfo?.readme).toBe("Test readme"); expect(promptInfo?.tags).toEqual(expect.arrayContaining(["test", "tag"])); - expect(promptInfo?.isPublic).toBe(false); + expect(promptInfo?.is_public).toBe(false); await client.deletePrompt(promptName); }); From 61211ab4957c34fd5f8bdbca0ba758d7b564402b Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Thu, 25 Jul 2024 18:50:21 -0700 Subject: [PATCH 10/16] tests --- js/src/client.ts | 22 +++++---- js/src/schemas.ts | 7 +-- js/src/tests/client.int.test.ts | 83 +++++++++++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 17 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index 3914a2a7f..57acdc2ec 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -606,7 +606,9 @@ export class Client { ); } - const items: T[] =transform ? transform(await response.json()) : await response.json(); + const items: T[] = transform + ? transform(await response.json()) + : await response.json(); if (items.length === 0) { break; @@ -3069,11 +3071,16 @@ export class Client { return this._likeOrUnlikePrompt(promptIdentifier, false); } - public async *listCommits(promptOwnerAndName: string): AsyncIterableIterator { - for await (const commits of this._getPaginated( + public async *listCommits( + promptOwnerAndName: string + ): AsyncIterableIterator { + for await (const commits of this._getPaginated< + PromptCommit, + ListCommitsResponse + >( `/commits/${promptOwnerAndName}/`, {} as URLSearchParams, - (res) => res.commits, + (res) => res.commits )) { yield* commits; } @@ -3129,12 +3136,11 @@ export class Client { isPublic?: boolean; isArchived?: boolean; sortField?: PromptSortField; - sortDirection?: "desc" | "asc"; query?: string; }): AsyncIterableIterator { const params = new URLSearchParams(); params.append("sort_field", options?.sortField ?? "updated_at"); - params.append("sort_direction", options?.sortDirection ?? "desc"); + params.append("sort_direction", "desc"); params.append("is_archived", (!!options?.isArchived).toString()); if (options?.isPublic !== undefined) { @@ -3148,7 +3154,7 @@ export class Client { for await (const prompts of this._getPaginated( "/repos", params, - (res) => res.repos, + (res) => res.repos )) { yield* prompts; } @@ -3412,7 +3418,7 @@ export class Client { }; } - public async pullPrompt( + public async _pullPrompt( promptIdentifier: string, options?: { includeModel?: boolean; diff --git a/js/src/schemas.ts b/js/src/schemas.ts index 350f114ba..99fdd1056 100644 --- a/js/src/schemas.ts +++ b/js/src/schemas.ts @@ -450,12 +450,7 @@ export interface ListCommitsResponse { total: number; } -export enum PromptSortField { - NumDownloads = "num_downloads", - NumViews = "num_views", - UpdatedAt = "updated_at", - NumLikes = "num_likes", -} +export type PromptSortField = "num_downloads" | "num_views" | "updated_at" | "num_likes" export interface LikePromptResponse { likes: number; diff --git a/js/src/tests/client.int.test.ts b/js/src/tests/client.int.test.ts index 824e47125..b7c0e5316 100644 --- a/js/src/tests/client.int.test.ts +++ b/js/src/tests/client.int.test.ts @@ -14,7 +14,10 @@ import { toArray, waitUntil, } from "./utils.js"; -import { ChatPromptTemplate } from "@langchain/core/prompts"; +import { ChatPromptTemplate, PromptTemplate } from "@langchain/core/prompts"; +import { ChatOpenAI } from "@langchain/openai"; +import { RunnableSequence } from "@langchain/core/runnables"; +import { load } from "langchain/load"; type CheckOutputsType = boolean | ((run: Run) => boolean); async function waitUntilRunFound( @@ -756,12 +759,65 @@ test.concurrent("Test run stats", async () => { test("Test list prompts", async () => { const client = new Client(); + // push 3 prompts + const promptName1 = `test_prompt_${uuidv4().slice(0, 8)}`; + const promptName2 = `test_prompt_${uuidv4().slice(0, 8)}`; + const promptName3 = `test_prompt_${uuidv4().slice(0, 8)}`; + + await client.pushPrompt(promptName1, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + isPublic: true, + }); + await client.pushPrompt(promptName2, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); + await client.pushPrompt(promptName3, { + object: ChatPromptTemplate.fromMessages( + [ + new SystemMessage({ content: "System message" }), + new HumanMessage({ content: "{{question}}" }), + ], + { templateFormat: "mustache" } + ), + }); + + // expect at least one of the prompts to have promptName1 const response = await client.listPrompts({ isPublic: true }); + let found = false; expect(response).toBeDefined(); for await (const prompt of response) { - console.log("this is what prompt looks like", prompt); expect(prompt).toBeDefined(); + if (prompt.repo_handle === promptName1) { + found = true; + } + } + expect(found).toBe(true); + + // expect the prompts to be sorted by updated_at + const response2 = client.listPrompts({ sortField: "updated_at" }); + expect(response2).toBeDefined(); + let lastUpdatedAt: number | undefined; + for await (const prompt of response2) { + expect(prompt.updated_at).toBeDefined(); + const currentUpdatedAt = new Date(prompt.updated_at).getTime(); + if (lastUpdatedAt !== undefined) { + expect(currentUpdatedAt).toBeLessThanOrEqual(lastUpdatedAt); + } + lastUpdatedAt = currentUpdatedAt; } + expect(lastUpdatedAt).toBeDefined(); }); test("Test get prompt", async () => { @@ -949,7 +1005,7 @@ test("Test push and pull prompt", async () => { tags: ["test", "tag"], }); - const pulledPrompt = await client.pullPrompt(promptName); + const pulledPrompt = await client._pullPrompt(promptName); expect(pulledPrompt).toBeDefined(); const promptInfo = await client.getPrompt(promptName); @@ -960,3 +1016,24 @@ test("Test push and pull prompt", async () => { await client.deletePrompt(promptName); }); + +test("Test pull prompt include model", async () => { + const client = new Client(); + const model = new ChatOpenAI({}); + const promptTemplate = PromptTemplate.fromTemplate( + "Tell me a joke about {topic}" + ); + const promptWithModel = promptTemplate.pipe(model); + + const promptName = `test_prompt_with_model_${uuidv4().slice(0, 8)}`; + await client.pushPrompt(promptName, { object: promptWithModel }); + + const pulledPrompt = await client._pullPrompt(promptName, { + includeModel: true, + }); + const rs: RunnableSequence = await load(pulledPrompt); + expect(rs).toBeDefined(); + expect(rs).toBeInstanceOf(RunnableSequence); + + await client.deletePrompt(promptName); +}); From b9c511d8bbd5a8adc3674e902800a13f4eb39292 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Thu, 25 Jul 2024 18:56:26 -0700 Subject: [PATCH 11/16] add langchain/openai --- js/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index a81a87b5d..b0234873a 100644 --- a/js/package.json +++ b/js/package.json @@ -66,7 +66,7 @@ "build:esm": "rm -f src/package.json && tsc --outDir dist/ && rm -rf dist/tests dist/**/tests", "build:cjs": "echo '{}' > src/package.json && tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs src/package.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests --testPathIgnorePatterns='\\.int\\.test.[tj]s' --testTimeout 30000", - "test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=\\.int\\.test.ts --testTimeout 100000", + "test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=client\\.int\\.test.ts --testTimeout 100000", "test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.cjs --testTimeout 100000", "watch:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --watch --config jest.config.cjs --testTimeout 100000", "lint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/", @@ -106,6 +106,7 @@ "@jest/globals": "^29.5.0", "@langchain/core": "^0.2.0", "@langchain/langgraph": "^0.0.19", + "@langchain/openai": "^0.2.5", "@tsconfig/recommended": "^1.0.2", "@types/jest": "^29.5.1", "@typescript-eslint/eslint-plugin": "^5.59.8", From 459e7bf493bc4119d9d5ae827d9dd1b48620df4c Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Thu, 25 Jul 2024 18:57:37 -0700 Subject: [PATCH 12/16] prettier --- js/src/schemas.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/src/schemas.ts b/js/src/schemas.ts index 99fdd1056..5692b8a86 100644 --- a/js/src/schemas.ts +++ b/js/src/schemas.ts @@ -450,7 +450,11 @@ export interface ListCommitsResponse { total: number; } -export type PromptSortField = "num_downloads" | "num_views" | "updated_at" | "num_likes" +export type PromptSortField = + | "num_downloads" + | "num_views" + | "updated_at" + | "num_likes"; export interface LikePromptResponse { likes: number; From d901757d62ca9de0dbfe4e96ea0e0c976b912f86 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Thu, 25 Jul 2024 19:01:03 -0700 Subject: [PATCH 13/16] rm test path --- js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index b0234873a..8dab2593f 100644 --- a/js/package.json +++ b/js/package.json @@ -66,7 +66,7 @@ "build:esm": "rm -f src/package.json && tsc --outDir dist/ && rm -rf dist/tests dist/**/tests", "build:cjs": "echo '{}' > src/package.json && tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs src/package.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests --testPathIgnorePatterns='\\.int\\.test.[tj]s' --testTimeout 30000", - "test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=client\\.int\\.test.ts --testTimeout 100000", + "test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=\\.int\\.test.ts --testTimeout 100000", "test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.cjs --testTimeout 100000", "watch:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --watch --config jest.config.cjs --testTimeout 100000", "lint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/", From 952db7eef648ffd5a0575d16e79d19064393e5d8 Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Fri, 26 Jul 2024 10:39:40 -0700 Subject: [PATCH 14/16] rm --- js/src/client.ts | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/js/src/client.ts b/js/src/client.ts index 57acdc2ec..bbf114346 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -3086,52 +3086,6 @@ export class Client { } } - public async *listProjects2({ - projectIds, - name, - nameContains, - referenceDatasetId, - referenceDatasetName, - referenceFree, - }: { - projectIds?: string[]; - name?: string; - nameContains?: string; - referenceDatasetId?: string; - referenceDatasetName?: string; - referenceFree?: boolean; - } = {}): AsyncIterable { - const params = new URLSearchParams(); - if (projectIds !== undefined) { - for (const projectId of projectIds) { - params.append("id", projectId); - } - } - if (name !== undefined) { - params.append("name", name); - } - if (nameContains !== undefined) { - params.append("name_contains", nameContains); - } - if (referenceDatasetId !== undefined) { - params.append("reference_dataset", referenceDatasetId); - } else if (referenceDatasetName !== undefined) { - const dataset = await this.readDataset({ - datasetName: referenceDatasetName, - }); - params.append("reference_dataset", dataset.id); - } - if (referenceFree !== undefined) { - params.append("reference_free", referenceFree.toString()); - } - for await (const projects of this._getPaginated( - "/sessions", - params - )) { - yield* projects; - } - } - public async *listPrompts(options?: { isPublic?: boolean; isArchived?: boolean; From 65c20cf8b414435e175588e4f45c875d40e92c6a Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Fri, 26 Jul 2024 10:55:21 -0700 Subject: [PATCH 15/16] nits --- js/src/client.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/js/src/client.ts b/js/src/client.ts index bbf114346..36ded0d64 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -3131,6 +3131,12 @@ export class Client { return null; } + if (!response.ok) { + throw new Error( + `Failed to get prompt: ${response.status} ${await response.text()}` + ); + } + const result = await response.json(); if (result.repo) { return result.repo as Prompt; @@ -3179,6 +3185,12 @@ export class Client { ...this.fetchOptions, }); + if (!response.ok) { + throw new Error( + `Failed to create prompt: ${response.status} ${await response.text()}` + ); + } + const { repo } = await response.json(); return repo as Prompt; } @@ -3372,6 +3384,13 @@ export class Client { }; } + /** + * + * This method should not be used directly, use `import { pull } from "langchain/hub"` instead. + * Using this method directly returns the JSON string of the prompt rather than a LangChain object. + * @private + * + */ public async _pullPrompt( promptIdentifier: string, options?: { @@ -3413,7 +3432,7 @@ export class Client { }); } - if (options?.object === null) { + if (!options?.object) { return await this._getPromptUrl(promptIdentifier); } From 2a81d658e6bab9cafe1728de3349c2b42625c2fe Mon Sep 17 00:00:00 2001 From: Maddy Adams Date: Fri, 26 Jul 2024 12:44:02 -0700 Subject: [PATCH 16/16] add openai --- js/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/js/package.json b/js/package.json index caef7b910..8a1598cd6 100644 --- a/js/package.json +++ b/js/package.json @@ -107,6 +107,7 @@ "langchain": "^0.2.10", "@langchain/core": "^0.2.17", "@langchain/langgraph": "^0.0.29", + "@langchain/openai": "^0.2.5", "@tsconfig/recommended": "^1.0.2", "@types/jest": "^29.5.1", "@typescript-eslint/eslint-plugin": "^5.59.8",