Skip to content

Commit 6fef769

Browse files
committed
📦 NEW: Parse primitive support
1 parent 0842c45 commit 6fef769

File tree

6 files changed

+246
-47
lines changed

6 files changed

+246
-47
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Composable AI
2+
3+
## The Developer Friendly Future of AI Infrastructure
4+
5+
In software engineering, composition is a powerful concept. It allows for building complex systems from simple, interchangeable parts. Think Legos, Docker containers, React components. Langbase extends this concept to AI infrastructure with our **Composable AI** stack using [Pipes][pipe] and [Memory][memory].
6+
7+
---
8+
9+
## Why Composable AI?
10+
11+
**Composable and personalized AI**: With Langbase, you can compose multiple models together into pipelines. It's easier to think about, easier to develop for, and each pipe lets you choose which model to use for each task. You can see cost of every step. And allow your customers to hyper-personalize.
12+
13+
**Effortlessly zero-config AI infra**: Maybe you want to use a smaller, domain-specific model for one task, and a larger general-purpose model for another task. Langbase makes it easy to use the right primitives and tools for each part of the job and provides developers with a zero-config composable AI infrastructure.
14+
15+
That's a nice way of saying, *you get a unicorn-scale API in minutes, not months*.
16+
17+
> **The most common problem** I hear about in Gen AI space is that my AI agents are too complex and I can't scale them, too much AI talking to AI. I don't have control, I don't understand the cost, and the impact of this change vs that. Time from new model to prod is too long. Feels static, my customers can't personalize it. ⌘ Langbase fixes all this. — [AA](https://www.linkedin.com/in/MrAhmadAwais/)
18+
19+
---
20+
21+
## Interactive Example: Composable AI Email Agent
22+
23+
But how does Composable AI work?
24+
25+
Here's an interactive example of a composable AI Email Agent: Classifies, summarizes, responds. Click to send a spam or valid email and check how composable it is: Swap any pipes, any LLM, hyper-personalize (you or your users), observe costs. Everything is composable.
26+
27+
28+
29+
## Example: Composable AI Email Agent
30+
31+
32+
I have built an AI email agent that can read my emails, understand the sentiment, summarize, and respond to them. Let's break it down to how it works, hint several pipes working together to make smart personalized decisions.
33+
34+
1. I created a pipe: `email-sentiment` — this one reads my emails to understand the sentiment
35+
2. `email-summarizer` pipe — it summarizes my emails so I can quickly understand them
36+
3. `email-decision-maker` pipe — should I respond? is it urgent? is it a newsletter?
37+
4. If `email-decision-maker` pipe says *yes*, then I need to respond. This invokes the final pipe
38+
5. `email-writer` pipe — writes a draft response to my emails with one of the eight formats I have
39+
40+
41+
## Why Composable AI is powerful?
42+
43+
Ah, the power of composition. I can swap out any of these pipes with a new one.
44+
45+
- **Flexibility**: Swap components without rewriting everything
46+
- **Reusability**: Build complex systems from simple, tested parts
47+
- **Scalability**: Optimize at the component level for better performance
48+
- **Observability**: Monitor and debug each step of your AI pipeline
49+
50+
51+
### Control flow
52+
53+
- Maybe I want to use a different sentiment analysis model
54+
- Or maybe I want to use a different summarizer when I'm on vacation
55+
- I can chose a different LLM (small or large) based on the task
56+
- BTW I definitely use a different `decision-maker` pipe on a busy day.
57+
58+
### Extensibility
59+
60+
- **Add more when needed**: I can also add more pipes to this pipeline. Maybe I want to add a pipe that checks my calendar or the weather before I respond to an email. You get the idea. Always bet on composition.
61+
- **Eight Formats to write emails**: And I have several formats. Because Pipes are composable, I have eight different versions of `email-writer` pipe. I have a pipe `email-pick-writer` that picks the correct pipe to draft a response with. Why? I talk to my friends differently than my investors, reports, managers, vendors — you name it.
62+
63+
64+
### Long-term memory and context awareness
65+
66+
- By the way, I have all my emails in an `emails-store` memory, which any of these pipes can refer to if needed. That's managed [semantic RAG][memory] over all the emails I have ever received.
67+
- And yes, my `emails-smart-spam` memory knows all the pesky smart spam emails that I don't want to see in my inbox.
68+
69+
### Cost & Observability
70+
71+
- Because each intent and action is mapped out Pipe — which is an excellent primitive for using LLMs, I can see everything related to cost, usage, and effectiveness of each pipe. I can see how many emails were processed, how many were responded to, how many were marked as spam, etc.
72+
- I can switch LLMs for any of these actions, [fork a pipe][fork], and see how it performs. I can version my pipes and see how the new version performs against the old one.
73+
- And we're just getting started …
74+
75+
### Why Developers Love It
76+
77+
- **Modular**: Build, test, and deploy pipes x memorysets independently
78+
- **Extensible**: API-first no dependency on a single language
79+
- **Version Control Friendly**: Track changes at the pipe level
80+
- **Cost-Effective**: Optimize resource usage for each AI task
81+
- **Stakeholder Friendly**: Collaborate with your team on each pipe and memory. All your R&D team, engineering, product, GTM (marketing, sales), and even stakeholders can collaborate on the same pipe. It's like a Google Doc x GitHub for AI. That's what makes it so powerful.
82+
83+
---
84+
85+
Each pipe and memory are like a docker container. You can have any number of pipes and memorysets.
86+
87+
Can't wait to share more exciting examples of composable AI. We're cookin!!
88+
89+
We'll share more on this soon. Follow us on [Twitter][x] and [LinkedIn][li] for updates.
90+
91+
[pipe]: /pipe/
92+
[memory]: /memory
93+
[signup]: https://langbase.fyi/awesome
94+
[x]: https://twitter.com/LangbaseInc
95+
[li]: https://www.linkedin.com/company/langbase/
96+
[email]: mailto:support@langbase.com?subject=Pipe-Quickstart&body=Ref:%20https://langbase.com/docs/pipe/quickstart
97+
[fork]: https://langbase.com/docs/features/fork
98+
99+
---
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import 'dotenv/config';
2+
import {Langbase} from 'langbase';
3+
import fs from 'fs';
4+
import path from 'path';
5+
6+
const langbase = new Langbase({
7+
apiKey: process.env.LANGBASE_API_KEY!,
8+
});
9+
10+
async function main() {
11+
const documentPath = path.join(
12+
process.cwd(),
13+
'examples',
14+
'parse',
15+
'composable-ai.md',
16+
);
17+
18+
const results = await langbase.parse({
19+
document: fs.readFileSync(documentPath),
20+
documentName: 'composable-ai.md',
21+
contentType: 'application/pdf',
22+
});
23+
24+
console.log(results);
25+
}
26+
27+
main();

examples/nodejs/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"tools.web-search": "npx tsx ./examples/tools/web-search.ts",
3232
"tools.crawl": "npx tsx ./examples/tools/crawl.ts",
3333
"embed": "npx tsx ./examples/embed/index.ts",
34-
"chunk": "npx tsx ./examples/chunk/index.ts"
34+
"chunk": "npx tsx ./examples/chunk/index.ts",
35+
"parse": "npx tsx ./examples/parse/index.ts"
3536
},
3637
"keywords": [],
3738
"author": "Ahmad Awais <me@AhmadAwais.com> (https://twitter.com/MrAhmadAwais)",

examples/nodejs/readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ npm run chunk
4040

4141
# embed
4242
npm run embed
43+
44+
# parse
45+
npm run parse
4346
```

packages/langbase/src/langbase/langbase.ts

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {convertDocToFormData} from '@/lib/utils/doc-to-formdata';
12
import {Request} from '../common/request';
23

34
export type Role = 'user' | 'assistant' | 'system' | 'tool';
@@ -191,6 +192,14 @@ export type EmbeddingModels =
191192
| 'cohere:embed-multilingual-light-v3.0'
192193
| 'google:text-embedding-004';
193194

195+
export type ContentType =
196+
| 'application/pdf'
197+
| 'text/plain'
198+
| 'text/markdown'
199+
| 'text/csv'
200+
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
201+
| 'application/vnd.ms-excel';
202+
194203
export interface MemoryCreateOptions {
195204
name: string;
196205
description?: string;
@@ -223,13 +232,7 @@ export interface MemoryUploadDocOptions {
223232
documentName: string;
224233
meta?: Record<string, string>;
225234
document: Buffer | File | FormData | ReadableStream;
226-
contentType:
227-
| 'application/pdf'
228-
| 'text/plain'
229-
| 'text/markdown'
230-
| 'text/csv'
231-
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
232-
| 'application/vnd.ms-excel';
235+
contentType: ContentType;
233236
}
234237

235238
export interface MemoryRetryDocEmbedOptions {
@@ -263,13 +266,7 @@ export interface MemoryListDocResponse {
263266
status_message: string | null;
264267
metadata: {
265268
size: number;
266-
type:
267-
| 'application/pdf'
268-
| 'text/plain'
269-
| 'text/markdown'
270-
| 'text/csv'
271-
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
272-
| 'application/vnd.ms-excel';
269+
type: ContentType;
273270
};
274271
enabled: boolean;
275272
chunk_size: number;
@@ -316,20 +313,25 @@ export type EmbedResponse = number[][];
316313
export interface ChunkOptions {
317314
document: Buffer | File | FormData | ReadableStream;
318315
documentName: string;
319-
contentType:
320-
| 'application/pdf'
321-
| 'text/plain'
322-
| 'text/markdown'
323-
| 'text/csv'
324-
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
325-
| 'application/vnd.ms-excel';
316+
contentType: ContentType;
326317
chunkMaxLength?: string;
327318
chunkOverlap?: string;
328319
separator?: string;
329320
}
330321

331322
export type ChunkResponse = string[];
332323

324+
export type ParseOptions = {
325+
document: Buffer | File | FormData | ReadableStream;
326+
documentName: string;
327+
contentType: ContentType;
328+
};
329+
330+
export type ParseResponse = {
331+
documentName: string;
332+
content: string;
333+
};
334+
333335
export class Langbase {
334336
private request: Request;
335337
private apiKey: string;
@@ -375,6 +377,7 @@ export class Langbase {
375377

376378
public embed: (options: EmbedOptions) => Promise<EmbedResponse>;
377379
public chunk: (options: ChunkOptions) => Promise<ChunkResponse>;
380+
public parse: (options: ParseOptions) => Promise<ParseResponse>;
378381

379382
constructor(options?: LangbaseOptions) {
380383
this.baseUrl = options?.baseUrl ?? 'https://api.langbase.com';
@@ -415,6 +418,7 @@ export class Langbase {
415418

416419
this.embed = this.generateEmbeddings.bind(this);
417420
this.chunk = this.chunkDocument.bind(this);
421+
this.parse = this.parseDocument.bind(this);
418422
}
419423

420424
private async runPipe(
@@ -714,32 +718,12 @@ export class Langbase {
714718
* @returns A promise that resolves to the chunked document response.
715719
*/
716720
private async chunkDocument(options: ChunkOptions): Promise<ChunkResponse> {
717-
let formData = new FormData();
718-
719-
if (options.document instanceof Buffer) {
720-
const documentBlob = new Blob([options.document], {
721-
type: options.contentType,
722-
});
723-
formData.append('document', documentBlob, options.documentName);
724-
} else if (options.document instanceof File) {
725-
formData.append('document', options.document, options.documentName);
726-
} else if (options.document instanceof FormData) {
727-
formData = options.document;
728-
} else if (options.document instanceof ReadableStream) {
729-
const chunks: Uint8Array[] = [];
730-
const reader = options.document.getReader();
731-
732-
while (true) {
733-
const {done, value} = await reader.read();
734-
if (done) break;
735-
chunks.push(value);
736-
}
737-
738-
const documentBlob = new Blob(chunks, {type: options.contentType});
739-
formData.append('document', documentBlob, options.documentName);
740-
}
721+
const formData = await convertDocToFormData({
722+
document: options.document,
723+
documentName: options.documentName,
724+
contentType: options.contentType,
725+
});
741726

742-
formData.append('documentName', options.documentName);
743727
if (options.chunkMaxLength)
744728
formData.append('chunkMaxLength', options.chunkMaxLength);
745729
if (options.chunkOverlap)
@@ -756,4 +740,34 @@ export class Langbase {
756740

757741
return response.json();
758742
}
743+
744+
/**
745+
* Parses a document using the Langbase API.
746+
*
747+
* @param options - The options for parsing the document
748+
* @param options.document - The document to be parsed
749+
* @param options.documentName - The name of the document
750+
* @param options.contentType - The content type of the document
751+
*
752+
* @returns A promise that resolves to the parse response from the API
753+
*
754+
* @throws {Error} If the API request fails
755+
*/
756+
private async parseDocument(options: ParseOptions): Promise<ParseResponse> {
757+
const formData = await convertDocToFormData({
758+
document: options.document,
759+
documentName: options.documentName,
760+
contentType: options.contentType,
761+
});
762+
763+
const response = await fetch(`${this.baseUrl}/v1/parse`, {
764+
method: 'POST',
765+
headers: {
766+
Authorization: `Bearer ${this.apiKey}`,
767+
},
768+
body: formData,
769+
});
770+
771+
return response.json();
772+
}
759773
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
/**
3+
* Converts various document formats to FormData.
4+
*
5+
* @param options - The conversion options
6+
* @param options.document - The document to convert. Can be Buffer, File, FormData or ReadableStream
7+
* @param options.documentName - The name of the document
8+
* @param options.contentType - The MIME type of the document
9+
*
10+
* @returns A Promise that resolves to FormData containing the document
11+
*
12+
* @example
13+
* ```ts
14+
* const buffer = Buffer.from('Hello World');
15+
* const formData = await convertDocToFormData({
16+
* document: buffer,
17+
* documentName: 'hello.txt',
18+
* contentType: 'text/plain'
19+
* });
20+
* ```
21+
*/
22+
export async function convertDocToFormData(options: {
23+
document: Buffer | File | FormData | ReadableStream;
24+
documentName: string;
25+
contentType: string;
26+
}) {
27+
let formData = new FormData();
28+
29+
if (options.document instanceof Buffer) {
30+
const documentBlob = new Blob([options.document], {
31+
type: options.contentType,
32+
});
33+
formData.append('document', documentBlob, options.documentName);
34+
} else if (options.document instanceof File) {
35+
formData.append('document', options.document, options.documentName);
36+
} else if (options.document instanceof FormData) {
37+
formData = options.document;
38+
} else if (options.document instanceof ReadableStream) {
39+
const chunks: Uint8Array[] = [];
40+
const reader = options.document.getReader();
41+
42+
while (true) {
43+
const {done, value} = await reader.read();
44+
if (done) break;
45+
chunks.push(value);
46+
}
47+
48+
const documentBlob = new Blob(chunks, {type: options.contentType});
49+
formData.append('document', documentBlob, options.documentName);
50+
}
51+
52+
formData.append('documentName', options.documentName);
53+
54+
return formData;
55+
}

0 commit comments

Comments
 (0)