diff --git a/client.ts b/client.ts index 50ec12d..15b11f0 100644 --- a/client.ts +++ b/client.ts @@ -53,7 +53,7 @@ export class CortexClient { async chat(opts: ClientCreateChatOptsSync): Promise; async chat(opts: ClientCreateChatOptsStreaming): Promise; async chat(opts: ClientCreateChatOptsSync | ClientCreateChatOptsStreaming): Promise { - if(opts.stream === true) { + if (opts.stream === true) { return Chat.create({ client: this.apiClient, cortex: opts.cortex, @@ -72,7 +72,7 @@ export class CortexClient { } } - async getChat(id: string){ + async getChat(id: string) { return Chat.get(this.apiClient, id); } @@ -80,7 +80,7 @@ export class CortexClient { async generateContent(opts: ClientCreateContentOptsStreaming): Promise async generateContent(opts: ClientCreateContentOptsSync | ClientCreateContentOptsStreaming) { // note: this if statement is annoying but is necessary to appropriately narrow the return type - if(opts.stream === true) { + if (opts.stream === true) { return Content.create({ client: this.apiClient, cortex: opts.cortex, @@ -105,7 +105,11 @@ export class CortexClient { return Content.get(this.apiClient, id, version); } - async listChats(){} + async listContent(paginationOptions?: { pageSize?: number; cursor?: string }) { + return Content.list(this.apiClient, paginationOptions); + } + + async listChats() { } async getCortex(name: string): Promise { return Cortex.get(this.apiClient, name) @@ -131,7 +135,7 @@ export class CortexClient { return Catalog.configure(this.apiClient, name, opts); } - async listCatalogs(){ + async listCatalogs() { return Catalog.list(this.apiClient); } diff --git a/content.test.ts b/content.test.ts index b3328b7..4c44f1e 100644 --- a/content.test.ts +++ b/content.test.ts @@ -10,12 +10,12 @@ const client = new CortexClient({ }); -test('e2e catalog, cortex, and sync content generation workflow', {timeout: 120000}, async () => { +test('e2e catalog, cortex, and sync content generation workflow', { timeout: 120000 }, async () => { client.configureOrg({ companyName: "Cortex Click", companyInfo: "Cortex Click provides an AI platform for go-to-market. Cortex click allows you to index your enterprise knowledge base, and create agents called Cortexes that automate sales and marketing processes like SEO, content writing, RFP generation, customer support, sales document genearation such as security questionairres and more.", - personality: [ "friendly and helpful", "expert sales and marketing professional", "experienced software developer"], + personality: ["friendly and helpful", "expert sales and marketing professional", "experienced software developer"], rules: ["never say anything disparaging about AI or LLMs", "do not offer discounts"], }) @@ -23,7 +23,7 @@ test('e2e catalog, cortex, and sync content generation workflow', {timeout: 1200 const config: CatalogConfig = { description: "this catalog contains documentation from the cortex click marketing website", - instructions: [ "user this data set to answer user questions about the cortex click platform" ] + instructions: ["user this data set to answer user questions about the cortex click platform"] }; // create @@ -50,16 +50,16 @@ test('e2e catalog, cortex, and sync content generation workflow', {timeout: 1200 await catalog.upsertDocuments(documents); const cortex = await client.configureCortex(`cortex-${Math.floor(Math.random() * 10000)}`, { - catalogs: [ catalog.name ], + catalogs: [catalog.name], friendlyName: "Cortex AI", - instructions: [ "answer questions about the cortex click AI GTM platform"], + instructions: ["answer questions about the cortex click AI GTM platform"], public: true, }); // create content const title = "Overview of the Cortex Click AI GTM Platform"; const prompt = "Write a blog post about the Cortex Click AI GTM Platform. Elaborate on scenarios, customers, and appropriate verticals. Make sure to mention the impact that AI can have on sales and marketing teams." - const content = await cortex.generateContent({title, prompt}); + const content = await cortex.generateContent({ title, prompt }); const originalContent = content.content; const originalTitle = content.title; expect(content.content.length).toBeGreaterThan(1); @@ -75,7 +75,7 @@ test('e2e catalog, cortex, and sync content generation workflow', {timeout: 1200 expect(getContent.commands.length).toBe(1); // edit content - const editedContent = await content.edit({title: "foo", content: "bar"}); + const editedContent = await content.edit({ title: "foo", content: "bar" }); expect(editedContent.content).toBe("bar"); expect(editedContent.title).toBe("foo"); expect(editedContent.version).toBe(1); @@ -107,12 +107,12 @@ test('e2e catalog, cortex, and sync content generation workflow', {timeout: 1200 await catalog.delete(); }); -test('test streaming content', {timeout: 120000}, async () => { +test('test streaming content', { timeout: 120000 }, async () => { client.configureOrg({ companyName: "Cortex Click", companyInfo: "Cortex Click provides an AI platform for go-to-market. Cortex click allows you to index your enterprise knowledge base, and create agents called Cortexes that automate sales and marketing processes like SEO, content writing, RFP generation, customer support, sales document genearation such as security questionairres and more.", - personality: [ "friendly and helpful", "expert sales and marketing professional", "experienced software developer"], + personality: ["friendly and helpful", "expert sales and marketing professional", "experienced software developer"], rules: ["never say anything disparaging about AI or LLMs", "do not offer discounts"], }) @@ -120,7 +120,7 @@ test('test streaming content', {timeout: 120000}, async () => { const config: CatalogConfig = { description: "this catalog contains documentation from the cortex click marketing website", - instructions: [ "user this data set to answer user questions about the cortex click platform" ] + instructions: ["user this data set to answer user questions about the cortex click platform"] }; // create @@ -147,9 +147,9 @@ test('test streaming content', {timeout: 120000}, async () => { await catalog.upsertDocuments(documents); const cortex = await client.configureCortex(`cortex-${Math.floor(Math.random() * 10000)}`, { - catalogs: [ catalog.name ], + catalogs: [catalog.name], friendlyName: "Cortex AI", - instructions: [ "answer questions about the cortex click AI GTM platform"], + instructions: ["answer questions about the cortex click AI GTM platform"], public: true, }); @@ -159,7 +159,7 @@ test('test streaming content', {timeout: 120000}, async () => { const statusStream = new Readable({ read() { } }); - const { content, contentStream } = await client.generateContent({ cortex, prompt, title, stream: true, statusStream}); + const { content, contentStream } = await client.generateContent({ cortex, prompt, title, stream: true, statusStream }); let fullContent = "" contentStream.on('data', (data) => { fullContent += data.toString(); @@ -172,7 +172,7 @@ test('test streaming content', {timeout: 120000}, async () => { statusStream.on('data', (data) => { const message = JSON.parse(data); expect(message.messageType).toBe("status"); - switch(message.step) { + switch (message.step) { case "plan": sawPlan = true; break; @@ -200,7 +200,7 @@ test('test streaming content', {timeout: 120000}, async () => { const refinedContentPromise = await contentResult.refine({ prompt: refinePrompt, stream: true }); let fullRefinedContent = ""; - refinedContentPromise.contentStream.on('data', (data)=> { + refinedContentPromise.contentStream.on('data', (data) => { fullRefinedContent += data.toString(); }); @@ -218,13 +218,13 @@ test('e2e content without any catalogs', { timeout: 120000 }, async () => { client.configureOrg({ companyName: "Cortex Click", companyInfo: "Cortex Click provides an AI platform for go-to-market. Cortex click allows you to index your enterprise knowledge base, and create agents called Cortexes that automate sales and marketing processes like SEO, content writing, RFP generation, customer support, sales document genearation such as security questionairres and more.", - personality: [ "friendly and helpful", "expert sales and marketing professional", "experienced software developer"], + personality: ["friendly and helpful", "expert sales and marketing professional", "experienced software developer"], rules: ["never say anything disparaging about AI or LLMs", "do not offer discounts"], }) const cortex = await client.configureCortex(`cortex-${Math.floor(Math.random() * 10000)}`, { friendlyName: "Cortex AI", - instructions: [ "answer questions about the cortex click AI GTM platform"], + instructions: ["answer questions about the cortex click AI GTM platform"], public: true, }); @@ -238,3 +238,43 @@ test('e2e content without any catalogs', { timeout: 120000 }, async () => { expect(content.version).toBe(0); expect(content.commands.length).toBe(1); }); + +test('list content', { timeout: 120000 }, async () => { + client.configureOrg({ + companyName: "Cortex Click", + companyInfo: "Cortex Click provides an AI platform for go-to-market. Cortex click allows you to index your enterprise knowledge base, and create agents called Cortexes that automate sales and marketing processes like SEO, content writing, RFP generation, customer support, sales document genearation such as security questionairres and more.", + personality: ["friendly and helpful", "expert sales and marketing professional", "experienced software developer"], + rules: ["never say anything disparaging about AI or LLMs", "do not offer discounts"], + }) + + const cortex = await client.configureCortex(`cortex-list-content-test`, { + friendlyName: "Cortex AI", + instructions: ["answer questions about the cortex click AI GTM platform"], + public: true, + }); + + // create content + const title = "Overview of the Cortex Click AI GTM Platform"; + const prompt = "Write a blog post about the Cortex Click AI GTM Platform. Elaborate on scenarios, customers, and appropriate verticals. Make sure to mention the impact that AI can have on sales and marketing teams." + const content = await cortex.generateContent({ title, prompt }); + + let contentList = (await client.listContent({ pageSize: 200 })); + // find the thing we just created + while (contentList.contents.filter(c => c.id === content.id).length === 0 && contentList.contents.length > 0) { + contentList = await contentList.nextPage(); + } + expect(contentList.contents.filter(c => c.id === content.id)).toHaveLength(1) // filter to just the stuff we created in this test + expect(contentList.contents.find(c => c.id === content.id)?.latestVersion).toBe(0); + + // check that it returns multiple items + await client.generateContent({ cortex, title: "foo", prompt: "bar" }); + const contentList3 = (await client.listContent()); + expect(contentList3.contents.length).toBeGreaterThan(1); + + // check that we can get the next page + const contentList4 = (await client.listContent({ pageSize: 1 })); + expect(contentList4.contents.length).toBe(1); + const contentList5 = await contentList4.nextPage(); + expect(contentList5.contents.length).toBe(1); + expect(contentList5.contents[0].id).not.toBe(contentList4.contents[0].id); +}) \ No newline at end of file diff --git a/content.ts b/content.ts index 1a6c6d4..251e040 100644 --- a/content.ts +++ b/content.ts @@ -52,6 +52,14 @@ export type EditContentOpts = { content?: string; } +export type ContentListResult = { + title: string; + latestVersion: number; + id: string; + Content(): Promise; +} + +export type ContentListRequestResult = { nextPage: () => Promise, contents: ContentListResult[] }; export class Content { get id() { @@ -86,7 +94,7 @@ export class Content { static async create(opts: CreateContentOptsStreaming): Promise; static async create(opts: CreateContentOptsSync | CreateContentOptsStreaming): Promise { // note: this if statement is annoying but is necessary to appropriately narrow the return type - if(isCreateContentOptsSync(opts)) { + if (isCreateContentOptsSync(opts)) { return this.createContentSync(opts); } else { return this.createContentStreaming(opts); @@ -95,16 +103,16 @@ export class Content { } private static async createContentSync(opts: CreateContentOptsSync): Promise { - const { client, cortex, title, prompt} = opts; - const res = await client.POST(`/content`, { cortex: cortex.name, title, prompt }); - const body = await res.json(); - - return new Content(client, body.id, body.title, body.content, body.commands, body.version); + const { client, cortex, title, prompt } = opts; + const res = await client.POST(`/content`, { cortex: cortex.name, title, prompt }); + const body = await res.json(); + + return new Content(client, body.id, body.title, body.content, body.commands, body.version); } private static async createContentStreaming(opts: CreateContentOptsStreaming): Promise { - const { client, cortex, title, prompt, stream} = opts; - const res = await client.POST(`/content`, { cortex: cortex.name, title, prompt, stream}); + const { client, cortex, title, prompt, stream } = opts; + const res = await client.POST(`/content`, { cortex: cortex.name, title, prompt, stream }); const reader = res.body!.getReader(); const decoder = new TextDecoder('utf-8'); @@ -115,17 +123,17 @@ export class Content { const readableStream = new Readable({ read() { } }); - + const contentPromise = processStream(reader, decoder, readableStream, opts.statusStream).then(content => { return new Content(client, id, title, content, commands, version, cortex.name); }); - return { contentStream: readableStream, content: contentPromise}; + return { contentStream: readableStream, content: contentPromise }; } static async get(client: CortexApiClient, id: string, version?: number): Promise { let res: Response; - if(version !== undefined) { + if (version !== undefined) { res = await client.GET(`/content/${id}/version/${version}`); } else { res = await client.GET(`/content/${id}`); @@ -159,7 +167,7 @@ export class Content { async refine(opts: RefineContentOptsSync): Promise; async refine(opts: RefineContentOptsStreaming): Promise; async refine(opts: RefineContentOptsSync | RefineContentOptsStreaming): Promise { - if(isRefineContentOptsSync(opts)) { + if (isRefineContentOptsSync(opts)) { return this.refineContentSync(opts); } else { return this.refineContentStreaming(opts); @@ -193,13 +201,13 @@ export class Content { const readableStream = new Readable({ read() { } }); - + const contentPromise = processStream(reader, decoder, readableStream, opts.statusStream).then(content => { this._content = content; return this; }); - return { contentStream: readableStream, content: contentPromise}; + return { contentStream: readableStream, content: contentPromise }; } async revert(version: number) { @@ -219,6 +227,33 @@ export class Content { return this; } + + static async list(client: CortexApiClient, paginationOpts?: { cursor?: string, pageSize?: number }): Promise { + const contents: ContentListResult[] = []; + + const query = new URLSearchParams(); + if (paginationOpts?.cursor) { + query.set("cursor", paginationOpts.cursor); + } + query.set("pageSize", (paginationOpts?.pageSize || 50).toString()); + const res = await client.GET(`/content?${query.toString()}`); + if (res.status !== 200) { + throw new Error(`Failed to list content: ${res.statusText}`); + } + const body = await res.json(); + for (let content of body.contents) { + contents.push({ + title: content.title, + latestVersion: content.latestVersion, + id: content.contentId, + Content: () => { return Content.get(client, content.contentId) } + }) + } + + const cursor = body.cursor; + const pageSize = paginationOpts?.pageSize; + return { contents, nextPage: async () => { return Content.list(client, { cursor, pageSize }) } }; + } } function isCreateContentOptsSync(opts: CreateContentOptsSync | CreateContentOptsStreaming): opts is CreateContentOptsSync { diff --git a/document.test.ts b/document.test.ts index 2a1830b..b5f8328 100644 --- a/document.test.ts +++ b/document.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { expect, test } from 'vitest'; import { CortexClient } from "./index"; import { CatalogConfig } from "./catalog"; import { FileDocument, JSONDocument, TextDocument } from './document'; @@ -129,7 +129,7 @@ test('Test upsertDocuments with files and catalog.truncate', { timeout: 20000 }, await catalog.delete(); }); -test('Test update documents', {timeout: 10000}, async () => { +test('Test update documents', { timeout: 10000 }, async () => { const catalogName = `catalog-${Math.floor(Math.random() * 10000)}` @@ -176,7 +176,7 @@ test('Test update documents', {timeout: 10000}, async () => { await catalog.delete(); }); -test('Test get and delete documents', {timeout: 10000}, async () => { +test('Test get and delete documents', { timeout: 10000 }, async () => { const catalogName = `catalog-${Math.floor(Math.random() * 10000)}` @@ -220,12 +220,12 @@ test('Test get and delete documents', {timeout: 10000}, async () => { await doc.delete(); docCount = await catalog.documentCount(); - expect(docCount).toBe(1); + expect(docCount).toBe(1); await catalog.delete(); }); -test('Test catalog.listDocuments', { timeout: 10000 } ,async () => { +test('Test catalog.listDocuments', { timeout: 10000 }, async () => { const catalogName = `catalog-${Math.floor(Math.random() * 10000)}` @@ -239,7 +239,7 @@ test('Test catalog.listDocuments', { timeout: 10000 } ,async () => { const docs: JSONDocument[] = [ ]; - for(let i = 0; i < 70; i++) { + for (let i = 0; i < 70; i++) { docs.push({ documentId: `${i}`, contentType: "json", @@ -266,7 +266,7 @@ test('Test catalog.listDocuments', { timeout: 10000 } ,async () => { expect(listDocsResult.documents.length).toBe(0); // paginate with page size: 70 - listDocsResult = await catalog.listDocuments({ page: 1, pageSize: 70}); + listDocsResult = await catalog.listDocuments({ page: 1, pageSize: 70 }); expect(listDocsResult.documents.length).toBe(70); listDocsResult = await listDocsResult.nextPage(); expect(listDocsResult.documents.length).toBe(0);