diff --git a/packages/cli/README.md b/packages/cli/README.md index caee3b7..1dfe1d6 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -9,54 +9,142 @@ The Langbase CLI is a command-line interface for Langbase. It provides a set of Integrate the Langbase Docs MCP server into your IDEs and Claude Desktop. #### Cursor + - Open Cursor settings - Navigate to the MCP settings - Click on the `+` button to add a new global MCP server - Paste the following configuration in the `mcp.json` file: + ```json { - "mcpServers": { - "Langbase": { - "command": "npx", - "args": ["@langbase/cli","docs-mcp-server"] - } - } + "mcpServers": { + "Langbase": { + "command": "npx", + "args": ["@langbase/cli", "docs-mcp-server"] + } + } } ``` #### Windsurf + - Navigate to Windsurf - Settings > Advanced Settings - You will find the option to Add Server - Click “Add custom server +” - Paste the following configuration in the `mcp_config.json` file: + ```json { - "mcpServers": { - "Langbase": { - "command": "npx", - "args": ["@langbase/cli", "docs-mcp-server"] - } - } + "mcpServers": { + "Langbase": { + "command": "npx", + "args": ["@langbase/cli", "docs-mcp-server"] + } + } } ``` #### Claude Desktop + - Open Claude Desktop File Menu - Navigate to the settings - Go to Developer Tab - Click on the Edit Config button - Paste the following configuration in the `claude_desktop_config.json` file: + ```json { - "mcpServers": { - "Langbase": { - "command": "npx", - "args": ["@langbase/cli", "docs-mcp-server"] - } - } + "mcpServers": { + "Langbase": { + "command": "npx", + "args": ["@langbase/cli", "docs-mcp-server"] + } + } } ``` +## CLI Commands + +Get started with the Langbase CLI by running the following command: + +```bash +npx @langbase/cli help +``` + +### Pipe Agent + +The CLI provides commands to manage your Langbase pipes. + +- Create a new pipe agent + +```bash +npx @langbase/cli pipe +``` + +- Run a pipe agent + +```bash +npx @langbase/cli pipe --run +``` + +- List all pipe agents + +```bash +npx @langbase/cli pipe --listPipes +``` + +- Update a pipe agent + +```bash +npx @langbase/cli pipe --update +``` + +### Memory + +The CLI provides commands to manage your Langbase memories. + +- Create a new memory + +```bash +npx @langbase/cli memory +``` + +- Upload a document to memory + +```bash +npx @langbase/cli memory --upload +``` + +- List all memories + +```bash +npx @langbase/cli memory --listMemories +``` + +- Retrieve chunks from memory + +```bash +npx @langbase/cli memory --retrieve +``` + +- List all documents in memory + +```bash +npx @langbase/cli memory --listDocs +``` + +- Retry embedding of a document in a memory + +```bash +npx @langbase/cli memory --embed +``` + +- Delete a memory + +```bash +npx @langbase/cli memory --delete +``` + ## Next steps - Read the [Langbase SDK documentation](https://langbase.com/docs/sdk) for more details diff --git a/packages/cli/package.json b/packages/cli/package.json index 393e810..84f2d29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "@clack/prompts": "^0.7.0", "@hono/node-server": "^1.13.1", "@hono/zod-openapi": "^0.16.0", + "@langbase/cli": "^0.1.4", "@modelcontextprotocol/sdk": "^1.8.0", "@sindresorhus/slugify": "^2.2.1", "camelcase": "^8.0.0", @@ -73,6 +74,7 @@ "hono": "^4.5.11", "js-tiktoken": "^1.0.14", "jsdom": "^24.1.0", + "langbase": "^1.1.55", "log-symbols": "^7.0.0", "lowdb": "^7.0.1", "meow": "^13.2.0", @@ -87,7 +89,8 @@ "uuid": "^10.0.0", "xlsx": "^0.18.5", "zod": "^3.23.8", - "zod-error": "^1.5.0" + "zod-error": "^1.5.0", + "zod-validation-error": "^3.3.0" }, "devDependencies": { "@langbase/eslint-config": "workspace:*", diff --git a/packages/cli/src/auth/index.ts b/packages/cli/src/auth/index.ts index f12c48b..631e653 100644 --- a/packages/cli/src/auth/index.ts +++ b/packages/cli/src/auth/index.ts @@ -112,5 +112,4 @@ export async function auth() { `Authentication successful. API key is stored in ${envFile}` ) ); - process.exit(0); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index aef60f3..6832ea2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,10 +3,20 @@ import { auth } from './auth'; import { build } from './build'; import { deploy } from './deploy'; import { docsMcpServer } from './docs-mcp-server'; +import { createPipe } from './pipe/create'; +import { runPipe } from './pipe/run'; +import { updatePipe } from './pipe/update'; +import { listPipes } from './pipe/list'; import cli from './utils/cli'; import debugMode from './utils/debug-mode'; import cliInit from './utils/init'; - +import { createMemory } from './memory/create'; +import { listMemories } from './memory/list'; +import { deleteMemory } from './memory/delete'; +import { uploadDocs } from './memory/upload-docs'; +import { embedDoc } from './memory/embed-doc'; +import { retriveFromMemory } from './memory/retrive'; +import { listDocs } from './memory/list-docs'; const { flags, input, showHelp } = cli; const { clear, debug } = flags; @@ -50,4 +60,61 @@ const flag = (flg: string): boolean => Boolean(flags[flg]); if (command('docs-mcp-server')) { await docsMcpServer(); } + + if ( + command('pipe') && + !flag('run') && + !flag('update') && + !flag('listPipes') + ) { + await createPipe(); + } + + if (command('pipe') && flag('run')) { + await runPipe(); + } + + if (command('pipe') && flag('update')) { + await updatePipe(); + } + + if (command('pipe') && flag('listPipes')) { + await listPipes(); + } + + if ( + command('memory') && + !flag('upload') && + !flag('embed') && + !flag('retrieve') && + !flag('listDocs') && + !flag('delete') && + !flag('listMemories') + ) { + await createMemory(); + } + + if (command('memory') && flag('listMemories')) { + await listMemories(); + } + + if (command('memory') && flag('delete')) { + await deleteMemory(); + } + + if (command('memory') && flag('upload')) { + await uploadDocs(); + } + + if (command('memory') && flag('embed')) { + await embedDoc(); + } + + if (command('memory') && flag('retrieve')) { + await retriveFromMemory(); + } + + if (command('memory') && flag('listDocs')) { + await listDocs(); + } })(); diff --git a/packages/cli/src/memory/create.ts b/packages/cli/src/memory/create.ts new file mode 100644 index 0000000..76cf38b --- /dev/null +++ b/packages/cli/src/memory/create.ts @@ -0,0 +1,136 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase, EmbeddingModels } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; +import { memoryNameSchema } from 'types/memory'; +import { fromZodError } from 'zod-validation-error'; + +export async function createMemory() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'Create a new memory' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the memory', + placeholder: 'cli-memory', + validate: value => { + const validatedName = + memoryNameSchema.safeParse(value); + if (!validatedName.success) { + const validationError = fromZodError( + validatedName.error + ).toString(); + return validationError; + } + return; + } + }), + description: () => + p.text({ + message: 'Description of the memory', + placeholder: 'This is a CLI memory' + }), + model: () => + p.select({ + message: 'Embedding Model', + options: [ + { + value: 'openai:text-embedding-3-large', + label: 'OpenAI Text Embedding 3 Large' + }, + { + value: 'cohere:embed-multilingual-v3.0', + label: 'Cohere Embed Multilingual v3.0' + }, + { + value: 'cohere:embed-multilingual-light-v3.0', + label: 'Cohere Embed Multilingual Light v3.0' + }, + { + value: 'google:text-embedding-004', + label: 'Google Text Embedding 004' + } + ] + }) as Promise, + topK: () => + p.text({ + message: 'Top K', + placeholder: '10', + validate: value => { + if (Number(value) < 1 || Number(value) > 100) { + return 'Top K must be between 1 and 100'; + } + return; + } + }), + chunkSize: () => + p.text({ + message: 'Chunk size', + placeholder: '10000', + validate: value => { + if ( + Number(value) < 1024 || + Number(value) > 300000 + ) { + return 'Chunk size must be between 1024 and 300000'; + } + return; + } + }), + chunkOverlap: () => + p.text({ + message: 'Chunk overlap', + placeholder: '1000', + validate: value => { + if (Number(value) < 100 || Number(value) > 1000) { + return 'Chunk overlap must be between 100 and 1000'; + } + return; + } + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then( + async ({ + name, + description, + model, + topK, + chunkSize, + chunkOverlap + }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Memory is being created...'); + + const memory = await langbase.memories.create({ + name, + description, + top_k: Number(topK), + chunk_size: Number(chunkSize), + chunk_overlap: Number(chunkOverlap), + embedding_model: model as EmbeddingModels + }); + + spinner.stop('Memory created successfully!'); + p.outro( + heading({ + text: 'MEMORY', + sub: `${memory.name} created successfully` + }) + ); + } + ); +} diff --git a/packages/cli/src/memory/delete.ts b/packages/cli/src/memory/delete.ts new file mode 100644 index 0000000..3a269d9 --- /dev/null +++ b/packages/cli/src/memory/delete.ts @@ -0,0 +1,57 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; +import { fromZodError } from 'zod-validation-error'; +import { memoryNameSchema } from 'types/memory'; + +export async function deleteMemory() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'Delete a memory' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the memory', + placeholder: 'cli-memory', + validate: value => { + const validatedName = + memoryNameSchema.safeParse(value); + if (!validatedName.success) { + const validationError = fromZodError( + validatedName.error + ).toString(); + return validationError; + } + return; + } + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Memory is being deleted...'); + + await langbase.memories.delete({ name }); + + spinner.stop('Memory deleted successfully!'); + p.outro( + heading({ + text: 'MEMORY DELETED', + sub: `${name} deleted successfully` + }) + ); + }); +} diff --git a/packages/cli/src/memory/embed-doc.ts b/packages/cli/src/memory/embed-doc.ts new file mode 100644 index 0000000..d5fdc1f --- /dev/null +++ b/packages/cli/src/memory/embed-doc.ts @@ -0,0 +1,62 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; +import { fromZodError } from 'zod-validation-error'; +import { memoryNameSchema } from 'types/memory'; + +export async function embedDoc() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'Retry embedding a document' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the memory', + placeholder: 'cli-memory', + validate: value => { + const validatedName = + memoryNameSchema.safeParse(value); + if (!validatedName.success) { + const validationError = fromZodError( + validatedName.error + ).toString(); + return validationError; + } + return; + } + }), + documentName: () => + p.text({ + message: 'Name of the document', + placeholder: 'cli-document' + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name, documentName }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Document is being embedded...'); + + const response = await langbase.memories.documents.embeddings.retry( + { + memoryName: name, + documentName: documentName + } + ); + + spinner.stop('Document embedded successfully!'); + console.log(response); + }); +} diff --git a/packages/cli/src/memory/list-docs.ts b/packages/cli/src/memory/list-docs.ts new file mode 100644 index 0000000..515807a --- /dev/null +++ b/packages/cli/src/memory/list-docs.ts @@ -0,0 +1,59 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; +import { fromZodError } from 'zod-validation-error'; +import { memoryNameSchema } from 'types/memory'; + +export async function listDocs() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'List all documents in a memory' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the memory', + placeholder: 'cli-memory', + validate: value => { + const validatedName = + memoryNameSchema.safeParse(value); + if (!validatedName.success) { + const validationError = fromZodError( + validatedName.error + ).toString(); + return validationError; + } + return; + } + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Listing all the documents...'); + + const response = await langbase.memories.documents.list({ + memoryName: name + }); + + spinner.stop('Documents listed successfully!'); + p.outro( + heading({ + text: `List of documents in ${name} memory`, + sub: `${response.map(doc => doc.name).join(', ')}` + }) + ); + }); +} diff --git a/packages/cli/src/memory/list.ts b/packages/cli/src/memory/list.ts new file mode 100644 index 0000000..9543c27 --- /dev/null +++ b/packages/cli/src/memory/list.ts @@ -0,0 +1,39 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; + +export async function listMemories() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'List all memories' })); + + try { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Listing all the memories...'); + + const response = await langbase.memories.list(); + + spinner.stop('Listed all the memories successfully!'); + + if (response.length === 0) { + console.log('No memories found.'); + } else { + p.outro( + heading({ + text: 'List of memories', + sub: `${response.map(memory => memory.name).join(', ')}` + }) + ); + } + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Unknown error occurred'; + p.cancel(`Error listing memories: ${errorMessage}`); + process.exit(1); + } +} diff --git a/packages/cli/src/memory/retrive.ts b/packages/cli/src/memory/retrive.ts new file mode 100644 index 0000000..ef37615 --- /dev/null +++ b/packages/cli/src/memory/retrive.ts @@ -0,0 +1,76 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; +import { fromZodError } from 'zod-validation-error'; +import { memoryNameSchema } from 'types/memory'; + +export async function retriveFromMemory() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'Retrive chunks from a memory' })); + await p + .group( + { + query: () => + p.text({ + message: 'Query', + placeholder: 'What is the capital of France?' + }), + name: () => + p.text({ + message: 'Name of the memory', + placeholder: 'cli-memory', + validate: value => { + const validatedName = + memoryNameSchema.safeParse(value); + if (!validatedName.success) { + const validationError = fromZodError( + validatedName.error + ).toString(); + return validationError; + } + return; + } + }), + topK: () => + p.text({ + message: 'Top K', + placeholder: '5', + validate: value => { + if (Number(value) < 1 || Number(value) > 100) { + return 'Top K must be between 1 and 100'; + } + return; + } + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name, query, topK }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Memory is being retrived...'); + + const response = await langbase.memories.retrieve({ + query: query, + memory: [ + { + name: name + } + ], + topK: Number(topK) + }); + + spinner.stop('Memory retrived successfully!'); + console.log(response); + }); +} diff --git a/packages/cli/src/memory/upload-docs.ts b/packages/cli/src/memory/upload-docs.ts new file mode 100644 index 0000000..7647f8b --- /dev/null +++ b/packages/cli/src/memory/upload-docs.ts @@ -0,0 +1,96 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; +import { fromZodError } from 'zod-validation-error'; +import { memoryNameSchema } from 'types/memory'; +import fs from 'fs'; +import path from 'path'; +import { ContentType } from 'langbase'; + +export async function uploadDocs() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'MEMORY', sub: 'Upload a document' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the memory', + placeholder: 'cli-memory', + validate: value => { + const validatedName = + memoryNameSchema.safeParse(value); + if (!validatedName.success) { + const validationError = fromZodError( + validatedName.error + ).toString(); + return validationError; + } + return; + } + }), + documentName: () => + p.text({ + message: 'Name of the document', + placeholder: 'cli-document' + }), + filePath: () => + p.text({ + message: 'Path to the document', + placeholder: '/path/to/your/file', + validate: value => { + if (!fs.existsSync(value)) { + return 'File does not exist'; + } + return; + } + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name, documentName, filePath }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Document is being uploaded...'); + + // Read the file + const document = fs.readFileSync(filePath); + + // Determine content type based on file extension + const ext = path.extname(filePath).toLowerCase(); + let contentType: ContentType = 'text/plain'; + + // Map common extensions to content types + const contentTypeMap: Record = { + '.txt': 'text/plain', + '.md': 'text/markdown', + '.pdf': 'application/pdf', + '.csv': 'text/csv', + '.xls': 'application/vnd.ms-excel', + '.xlsx': + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }; + + contentType = contentTypeMap[ext] || 'text/plain'; + + await langbase.memories.documents.upload({ + memoryName: name, + documentName: documentName, + document: document, + contentType: contentType + }); + + spinner.stop('Document uploaded successfully!'); + console.log(`${documentName} uploaded successfully!`); + }); +} diff --git a/packages/cli/src/pipe/create.ts b/packages/cli/src/pipe/create.ts new file mode 100644 index 0000000..de44488 --- /dev/null +++ b/packages/cli/src/pipe/create.ts @@ -0,0 +1,81 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { pipeNameSchema, PipeStatus } from '../../types/pipe'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; + +export async function createPipe() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'PIPE AGENT', sub: 'Create a new pipe agent' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the pipe agent', + placeholder: 'cli-pipe-agent', + validate: value => { + const result = pipeNameSchema.safeParse(value); + if (!result.success) { + return result.error.issues[0].message; + } + return; + } + }), + description: () => + p.text({ + message: 'Description of the pipe', + placeholder: 'This is a CLI pipe agent' + }), + status: () => + p.select({ + message: 'Status of the pipe', + options: [ + { value: 'public', label: 'Public' }, + { value: 'private', label: 'Private' } + ] + }) as Promise, + systemPrompt: () => + p.text({ + message: 'System prompt', + placeholder: 'You are a helpful AI assistant.', + initialValue: 'You are a helpful AI assistant.' + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name, description, systemPrompt, status }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Pipe is being created...'); + + const pipe = await langbase.pipes.create({ + name, + status, + description, + messages: [ + { + role: 'system', + content: systemPrompt + } + ] + }); + + spinner.stop('Pipe created successfully!'); + p.outro( + heading({ + text: 'PIPE', + sub: `${pipe.name} created successfully` + }) + ); + }); +} diff --git a/packages/cli/src/pipe/list.ts b/packages/cli/src/pipe/list.ts new file mode 100644 index 0000000..bb8b184 --- /dev/null +++ b/packages/cli/src/pipe/list.ts @@ -0,0 +1,39 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; + +export async function listPipes() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'PIPE AGENT', sub: 'List all pipe agents' })); + + try { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Listing all the pipe agents...'); + + const response = await langbase.pipes.list(); + + spinner.stop('Listed all the pipe agents successfully!'); + + if (response.length === 0) { + console.log('No pipe agents found.'); + } else { + p.outro( + heading({ + text: 'List of pipe agents', + sub: `${response.map(pipe => pipe.name).join(', ')}` + }) + ); + } + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Unknown error occurred'; + p.cancel(`Error listing pipe agents: ${errorMessage}`); + process.exit(1); + } +} diff --git a/packages/cli/src/pipe/run.ts b/packages/cli/src/pipe/run.ts new file mode 100644 index 0000000..ea14a1f --- /dev/null +++ b/packages/cli/src/pipe/run.ts @@ -0,0 +1,101 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { pipeNameSchema } from '../../types/pipe'; +import { getRunner, Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; + +export async function runPipe() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'PIPE AGENT', sub: 'Run a pipe agent' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the pipe agent', + placeholder: 'cli-pipe-agent', + validate: value => { + const result = pipeNameSchema.safeParse(value); + if (!result.success) { + return result.error.issues[0].message; + } + return; + } + }), + stream: () => + p.select({ + message: 'Stream', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' } + ] + }) as Promise, + prompt: () => + p.text({ + message: 'Prompt', + placeholder: 'What is the capital of France?' + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name, prompt, stream }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Running the pipe agent...'); + + if (stream) { + const { stream: streamResponse } = await langbase.pipes.run({ + name, + stream, + messages: [ + { + role: 'user', + content: prompt + } + ] + }); + + const runner = getRunner(streamResponse); + runner.on('connect', () => { + console.log('Stream started.\n'); + }); + + runner.on('content', content => { + process.stdout.write(content); + }); + + runner.on('end', () => { + console.log('\nStream ended.'); + }); + + runner.on('error', error => { + console.error(error); + }); + + spinner.stop('Pipe agent run successfully!'); + } else { + const response = await langbase.pipes.run({ + name, + stream, + messages: [ + { + role: 'user', + content: prompt + } + ] + }); + + spinner.stop('Pipe agent run successfully!'); + console.log(response.completion); + } + }); +} diff --git a/packages/cli/src/pipe/update.ts b/packages/cli/src/pipe/update.ts new file mode 100644 index 0000000..1885502 --- /dev/null +++ b/packages/cli/src/pipe/update.ts @@ -0,0 +1,77 @@ +import { heading } from '@/utils/heading'; +import * as p from '@clack/prompts'; +import { pipeNameSchema } from '../../types/pipe'; +import { Langbase } from 'langbase'; +import { getApiKey } from '@/utils/get-langbase-api-key'; + +export async function updatePipe() { + const apiKey = await getApiKey(); + + p.intro(heading({ text: 'PIPE AGENT', sub: 'Update a pipe agent' })); + await p + .group( + { + name: () => + p.text({ + message: 'Name of the pipe agent', + placeholder: 'cli-pipe-agent', + validate: value => { + const result = pipeNameSchema.safeParse(value); + if (!result.success) { + return result.error.issues[0].message; + } + return; + } + }), + description: () => + p.text({ + message: 'Description of the pipe', + placeholder: 'This is a CLI pipe agent' + }), + systemPrompt: () => + p.text({ + message: 'System prompt', + placeholder: 'You are a helpful AI assistant.' + }), + temperature: () => + p.text({ + message: 'Temperature', + placeholder: '0.5' + }) + }, + { + onCancel: () => { + p.cancel('Operation cancelled.'); + process.exit(0); + } + } + ) + .then(async ({ name, description, systemPrompt, temperature }) => { + const langbase = new Langbase({ + apiKey: apiKey + }); + + const spinner = p.spinner(); + spinner.start('Updating the pipe agent...'); + + const pipe = await langbase.pipes.update({ + name, + description, + messages: [ + { + role: 'system', + content: systemPrompt + } + ], + temperature: Number(temperature) + }); + + spinner.stop('Pipe updated successfully!'); + p.outro( + heading({ + text: 'PIPE', + sub: `${pipe.name} updated successfully` + }) + ); + }); +} diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index bbedfa2..747eff0 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -13,7 +13,53 @@ const flags = { default: false, shortFlag: `d`, desc: `Print debug info` + }, + run: { + type: `boolean`, + default: false, + desc: `Run a pipe agent` + }, + update: { + type: `boolean`, + default: false, + desc: `Update a pipe agent` + }, + listPipes: { + type: `boolean`, + default: false, + desc: `List all pipe agents` + }, + listMemories: { + type: `boolean`, + default: false, + desc: `List all memories` + }, + upload: { + type: `boolean`, + default: false, + desc: `Upload a document to memory` + }, + retrieve: { + type: `boolean`, + default: false, + desc: `Retrieve chunks from memory` + }, + listDocs: { + type: `boolean`, + default: false, + desc: `List all documents in memory` + }, + embed: { + type: `boolean`, + default: false, + desc: `Retry embedding of a document in a memory` + }, + delete: { + type: `boolean`, + default: false, + desc: `Delete a memory` } + // agent: { // type: `string`, // shortFlag: `a`, @@ -32,6 +78,19 @@ const flags = { }; const commands = { + pipe: { + desc: `Create a pipe agent`, + flags: { + create: { + type: `boolean`, + default: false, + desc: `Create a pipe agent` + } + } + }, + memory: { + desc: `Create a memory` + }, 'docs-mcp-server': { desc: `Start the Langbase docs MCP server` }, diff --git a/packages/cli/src/utils/get-langbase-api-key.ts b/packages/cli/src/utils/get-langbase-api-key.ts new file mode 100644 index 0000000..c6bb271 --- /dev/null +++ b/packages/cli/src/utils/get-langbase-api-key.ts @@ -0,0 +1,34 @@ +import { auth } from '@/auth'; +import fs from 'fs/promises'; +import path from 'path'; +import { findEnvFile } from './find-env'; + +export async function getApiKey(): Promise { + const envFileName = await findEnvFile(); + if (!envFileName) { + await auth(); + // After auth, try to find the env file again + const newEnvFileName = await findEnvFile(); + if (!newEnvFileName) { + console.error('Failed to find .env file after authentication'); + process.exit(1); + } + + return getApiKey(); + } + + // Read the content of the env file + const envFilePath = path.join(process.cwd(), envFileName); + const envContent = await fs.readFile(envFilePath, 'utf-8'); + + // Extract the API key from the env content + const apiKeyMatch = envContent.match(/LANGBASE_API_KEY=([^\n]+)/); + if (!apiKeyMatch) { + console.error('LANGBASE_API_KEY not found in .env file'); + process.exit(1); + } + + const apiKey = apiKeyMatch[1]; + + return apiKey; +} diff --git a/packages/cli/types/memory.ts b/packages/cli/types/memory.ts new file mode 100644 index 0000000..433c9cc --- /dev/null +++ b/packages/cli/types/memory.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const memoryNameSchema = z + .string() + .min(3, 'Memory name must be at least 3 characters long') + .max(50, 'Memory name must not exceed 50 characters') + .regex( + /^[a-zA-Z0-9.-]+$/, + 'Memory name can only contain letters, numbers, dots, and hyphens' + ); diff --git a/packages/cli/types/pipe.ts b/packages/cli/types/pipe.ts new file mode 100644 index 0000000..3241ac9 --- /dev/null +++ b/packages/cli/types/pipe.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export type PipeStatus = 'public' | 'private'; + +export const pipeNameSchema = z + .string() + .min(3, 'Pipe name must be at least 3 characters long') + .max(50, 'Pipe name must not exceed 50 characters') + .regex( + /^[a-zA-Z0-9.-]+$/, + 'Pipe name can only contain letters, numbers, dots, and hyphens' + );