From 6bec4f748cc42b2c64b460bf76da8586e4793369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rog=C3=A9rio=20Chaves?= Date: Fri, 4 Aug 2023 07:08:32 +0200 Subject: [PATCH] Rename the whole library from litechain to langstream. Closes #4 --- .gitignore | 2 +- Makefile | 4 +- README.md | 70 +- docs/docs/chain-basics/composing_chains.md | 235 ---- docs/docs/chain-basics/custom_chains.md | 78 -- docs/docs/chain-basics/index.md | 40 - docs/docs/chain-basics/type_signatures.md | 64 -- .../docs/chain-basics/working_with_streams.md | 120 -- docs/docs/examples/index.md | 2 +- .../openai-function-call-extract-schema.ipynb | 14 +- docs/docs/examples/qa-over-documents.ipynb | 112 +- docs/docs/examples/serve-with-fastapi.ipynb | 26 +- .../examples/weather-bot-error-handling.ipynb | 56 +- docs/docs/examples/weather-bot.ipynb | 32 +- docs/docs/intro.md | 22 +- docs/docs/llms/gpt4all.md | 18 +- docs/docs/llms/index.md | 4 +- docs/docs/llms/memory.md | 32 +- docs/docs/llms/open_ai.md | 32 +- docs/docs/llms/open_ai_functions.md | 16 +- docs/docs/llms/zero_temperature.md | 2 +- .../_category_.json | 0 docs/docs/stream-basics/composing_streams.md | 235 ++++ docs/docs/stream-basics/custom_streams.md | 78 ++ .../error_handling.md | 20 +- docs/docs/stream-basics/index.md | 40 + docs/docs/stream-basics/type_signatures.md | 64 ++ .../why_streams.md | 4 +- .../stream-basics/working_with_streams.md | 120 ++ docs/docs/ui/chainlit.md | 50 +- docs/docusaurus.config.js | 16 +- docs/package-lock.json | 44 +- docs/pdoc_template/html.mako | 14 +- docs/sidebars.js | 2 +- langstream/__init__.py | 128 +++ langstream/contrib/__init__.py | 26 + .../contrib/llms/__init__.py | 0 .../contrib/llms/gpt4all_stream.py | 20 +- .../contrib/llms/open_ai.py | 66 +- {litechain => langstream}/core/__init__.py | 0 .../chain.py => langstream/core/stream.py | 408 +++---- {litechain => langstream}/utils/__init__.py | 0 {litechain => langstream}/utils/_typing.py | 0 .../utils/async_generator.py | 4 +- langstream/utils/stream.py | 187 +++ litechain/__init__.py | 128 --- litechain/contrib/__init__.py | 26 - litechain/utils/chain.py | 187 --- requirements.dev.txt | 1 + settings.ini | 2 +- setup.py | 12 +- tests/contrib/llms/test_gpt4all_chain.py | 62 - tests/core/test_chain.py | 1008 ----------------- 53 files changed, 1432 insertions(+), 2501 deletions(-) delete mode 100644 docs/docs/chain-basics/composing_chains.md delete mode 100644 docs/docs/chain-basics/custom_chains.md delete mode 100644 docs/docs/chain-basics/index.md delete mode 100644 docs/docs/chain-basics/type_signatures.md delete mode 100644 docs/docs/chain-basics/working_with_streams.md rename docs/docs/{chain-basics => stream-basics}/_category_.json (100%) create mode 100644 docs/docs/stream-basics/composing_streams.md create mode 100644 docs/docs/stream-basics/custom_streams.md rename docs/docs/{chain-basics => stream-basics}/error_handling.md (51%) create mode 100644 docs/docs/stream-basics/index.md create mode 100644 docs/docs/stream-basics/type_signatures.md rename docs/docs/{chain-basics => stream-basics}/why_streams.md (78%) create mode 100644 docs/docs/stream-basics/working_with_streams.md create mode 100644 langstream/__init__.py create mode 100644 langstream/contrib/__init__.py rename {litechain => langstream}/contrib/llms/__init__.py (100%) rename litechain/contrib/llms/gpt4all_chain.py => langstream/contrib/llms/gpt4all_stream.py (82%) rename {litechain => langstream}/contrib/llms/open_ai.py (80%) rename {litechain => langstream}/core/__init__.py (100%) rename litechain/core/chain.py => langstream/core/stream.py (50%) rename {litechain => langstream}/utils/__init__.py (100%) rename {litechain => langstream}/utils/_typing.py (100%) rename {litechain => langstream}/utils/async_generator.py (99%) create mode 100644 langstream/utils/stream.py delete mode 100644 litechain/__init__.py delete mode 100644 litechain/contrib/__init__.py delete mode 100644 litechain/utils/chain.py delete mode 100644 tests/contrib/llms/test_gpt4all_chain.py delete mode 100644 tests/core/test_chain.py diff --git a/.gitignore b/.gitignore index 6c670bb..c818ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ chainlit.md !docs/**/chainlit.md test*.py build/ -litechain.egg-info/ +langstream.egg-info/ .chroma # Generated markdown files from jupyter notebooks diff --git a/Makefile b/Makefile index ca4d088..dfd5cfa 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ test-integration: PYTHONPATH=$$PYTHONPATH:. pytest -s -m integration $(filter-out $@,$(MAKECMDGOALS)) doctest: - PYTHONPATH=$$PYTHONPATH:. pytest --doctest-modules litechain/utils && PYTHONPATH=$PYTHONPATH:. pytest --doctest-modules litechain/core && PYTHONPATH=$PYTHONPATH:. pytest --doctest-modules litechain/contrib/llms + PYTHONPATH=$$PYTHONPATH:. pytest --doctest-modules langstream/utils && PYTHONPATH=$PYTHONPATH:. pytest --doctest-modules langstream/core && PYTHONPATH=$PYTHONPATH:. pytest --doctest-modules langstream/contrib/llms nbtest: nbdoc_test --fname docs/docs/ @@ -16,7 +16,7 @@ docs: make pdocs && make nbdocs && cd docs && npm run build pdocs: - pdoc --html -o ./docs/static/reference --template-dir ./docs/pdoc_template litechain --force + pdoc --html -o ./docs/static/reference --template-dir ./docs/pdoc_template langstream --force nbdocs: nbdoc_build --srcdir docs/docs diff --git a/README.md b/README.md index 20cd166..3e0f68f 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,38 @@ -# 🪽🔗 LiteChain +# 🪽🔗 LangStream [![](https://dcbadge.vercel.app/api/server/48ZM5KkKgw?style=flat)](https://discord.gg/48ZM5KkKgw) -[![Release Notes](https://img.shields.io/github/release/rogeriochaves/litechain)](https://pypi.org/project/litechain/) -[![tests](https://github.com/rogeriochaves/litechain/actions/workflows/run_tests.yml/badge.svg)](https://github.com/rogeriochaves/litechain/actions/workflows/run_tests.yml) -[![docs](https://github.com/rogeriochaves/litechain/actions/workflows/publish_docs.yml/badge.svg)](https://github.com/rogeriochaves/litechain/actions/workflows/publish_docs.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/rogeriochaves/litechain/blob/main/LICENSE) +[![Release Notes](https://img.shields.io/github/release/rogeriochaves/langstream)](https://pypi.org/project/langstream/) +[![tests](https://github.com/rogeriochaves/langstream/actions/workflows/run_tests.yml/badge.svg)](https://github.com/rogeriochaves/langstream/actions/workflows/run_tests.yml) +[![docs](https://github.com/rogeriochaves/langstream/actions/workflows/publish_docs.yml/badge.svg)](https://github.com/rogeriochaves/langstream/actions/workflows/publish_docs.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/rogeriochaves/langstream/blob/main/LICENSE) -LiteChain is a lighter alternative to LangChain for building LLMs application, instead of having a massive amount of features and classes, LiteChain focuses on having a single small core, that is easy to learn, easy to adapt, well documented, fully typed and truly composable. +LangStream is a lighter alternative to LangChain for building LLMs application, instead of having a massive amount of features and classes, LangStream focuses on having a single small core, that is easy to learn, easy to adapt, well documented, fully typed and truly composable, with Streams instead of chains as the building block. -[Documentation](https://rogeriochaves.github.io/litechain) +[Documentation](https://rogeriochaves.github.io/langstream) # Quick Install ``` -pip install litechain +pip install langstream ``` -# 🔗 The Chain building block +# 🔗 The Stream building block -The Chain is the building block for LiteChain, an LLM is a Chain, an output parser is a Chain, a group of chains can be composed as another Chain, it's [Chains all the way down](https://en.wikipedia.org/wiki/Turtles_all_the_way_down). +The Stream is the building block for LangStream, an LLM is a Stream, an output parser is a Stream, a group of streams can be composed as another Stream, it's [Streams all the way down](https://en.wikipedia.org/wiki/Turtles_all_the_way_down). -Take a look at [the documentation](https://rogeriochaves.github.io/litechain) for guides on building on chains and building LLM applications, or go straight to [the reference](https://rogeriochaves.github.io/litechain/reference/litechain/index.html#chain) for the core concept and modules available. +Take a look at [the documentation](https://rogeriochaves.github.io/langstream) for guides on building on streams and building LLM applications, or go straight to [the reference](https://rogeriochaves.github.io/langstream/reference/langstream/index.html#stream) for the core concept and modules available. # Quick Example Here is a ChatBot that answers anything you ask using only emojis: ```python -from litechain.contrib import OpenAIChatChain, OpenAIChatMessage, OpenAIChatDelta +from langstream.contrib import OpenAIChatStream, OpenAIChatMessage, OpenAIChatDelta from typing import Iterable -# Creating a GPT-4 EmojiChain -emoji_chain = OpenAIChatChain[str, OpenAIChatDelta]( - "EmojiChain", +# Creating a GPT-4 EmojiStream +emoji_stream = OpenAIChatStream[str, OpenAIChatDelta]( + "EmojiStream", lambda user_message: [ OpenAIChatMessage( role="user", content=f"{user_message}. Reply in emojis" @@ -43,25 +43,25 @@ emoji_chain = OpenAIChatChain[str, OpenAIChatDelta]( ) # Now interacting with it -async for output in emoji_chain("Hey there, how is it going?"): +async for output in emoji_stream("Hey there, how is it going?"): print(output.data.content, end="") #=> 👋😊👍💻🌞 -async for output in emoji_chain("What is answer to the ultimate question of life, the universe, and everything?"): +async for output in emoji_stream("What is answer to the ultimate question of life, the universe, and everything?"): print(output.data.content, end="") #=> 4️⃣2️⃣ ``` -In this simple example, we are creating a [GPT4 Chain](https://rogeriochaves.github.io/litechain/reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatChain) that takes the user message and appends `". Reply in emojis"` to it for building the prompt, following the [OpenAI chat structure](https://rogeriochaves.github.io/litechain/reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatMessage) and with [zero temperature](https://rogeriochaves.github.io/litechain/docs/llms/zero_temperature). +In this simple example, we are creating a [GPT4 Stream](https://rogeriochaves.github.io/langstream/reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatStream) that takes the user message and appends `". Reply in emojis"` to it for building the prompt, following the [OpenAI chat structure](https://rogeriochaves.github.io/langstream/reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatMessage) and with [zero temperature](https://rogeriochaves.github.io/langstream/docs/llms/zero_temperature). -Then, as you can see, we have an async loop going over each token output from `emoji_chain`. In LiteChain, everything is an async stream using Python's `AsyncGenerator` class, and the most powerful part of it, is that you can connect those streams by composing two Chains together: +Then, as you can see, we have an async loop going over each token output from `emoji_stream`. In LangStream, everything is an async stream using Python's `AsyncGenerator` class, and the most powerful part of it, is that you can connect those streams by composing two Streams together: ```python -# Creating another Chain to translate back from emoji -translator_chain = OpenAIChatChain[Iterable[OpenAIChatDelta], OpenAIChatDelta]( - "TranslatorChain", +# Creating another Stream to translate back from emoji +translator_stream = OpenAIChatStream[Iterable[OpenAIChatDelta], OpenAIChatDelta]( + "TranslatorStream", lambda emoji_tokens: [ OpenAIChatMessage( role="user", content=f"Translate this emoji message {[token.content for token in emoji_tokens]} to plain english" @@ -70,33 +70,33 @@ translator_chain = OpenAIChatChain[Iterable[OpenAIChatDelta], OpenAIChatDelta]( model="gpt-4", ) -# Connecting the two Chains together -chain = emoji_chain.and_then(translator_chain) +# Connecting the two Streams together +stream = emoji_stream.and_then(translator_stream) # Trying out the whole flow -async for output in chain("Hey there, how is it going?"): +async for output in stream("Hey there, how is it going?"): print(output.data.content, end="") #=> 👋😊👍💻🌞"Hello, have a nice day working on your computer!" ``` -As you can see, it's easy enough to connect two Chains together using the `and_then` function. There are other functions available for composition such as `map`, `collect`, `join` and `gather`, they form the small set of abstractions you need to learn to build complex Chain compositions for your application, and they behave as you would expect if you have Function Programming knowledge. You can read all about it in the [reference](https://rogeriochaves.github.io/litechain/reference/litechain/index.html). Once you learn those functions, any Chain will follow the same patterns, enabling you to build complex LLM applications. +As you can see, it's easy enough to connect two Streams together using the `and_then` function. There are other functions available for composition such as `map`, `collect`, `join` and `gather`, they form the small set of abstractions you need to learn to build complex Stream compositions for your application, and they behave as you would expect if you have Function Programming knowledge. You can read all about it in the [reference](https://rogeriochaves.github.io/langstream/reference/langstream/index.html). Once you learn those functions, any Stream will follow the same patterns, enabling you to build complex LLM applications. -As you may also have noticed, Chains accept type signatures, EmojiChain has the type `[str, OpenAIChatDelta]`, while TranslatorChain has the type `[Iterable[OpenAIChatDelta], OpenAIChatDelta]`, those mean respectively the *input* and *output* types of each Chain. Since the EmojiChain is taking user output, it simply takes a `str` as input, and since it's using OpenAI Chat API with GPT-4, it produces `OpenAIChatDelta`, which is [the tokens that GPT-4 produces one at a time](https://rogeriochaves.github.io/litechain/reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatDelta). TranslatorChain then takes `Iterable[OpenAIChatDelta]` as input, since it's connected with the output from EmojiChain, it takes the full list of the generated tokens to later extract their content and form its own prompt. +As you may also have noticed, Streams accept type signatures, EmojiStream has the type `[str, OpenAIChatDelta]`, while TranslatorStream has the type `[Iterable[OpenAIChatDelta], OpenAIChatDelta]`, those mean respectively the *input* and *output* types of each Stream. Since the EmojiStream is taking user output, it simply takes a `str` as input, and since it's using OpenAI Chat API with GPT-4, it produces `OpenAIChatDelta`, which is [the tokens that GPT-4 produces one at a time](https://rogeriochaves.github.io/langstream/reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatDelta). TranslatorStream then takes `Iterable[OpenAIChatDelta]` as input, since it's connected with the output from EmojiStream, it takes the full list of the generated tokens to later extract their content and form its own prompt. -The type signatures are an important part of LiteChain, having them can save a lot of time preventing bugs and debugging issues caused for example when Chain B is not expecting the output of Chain A. Using an editor like VSCode with PyLance allows you to get warned that Chain A doesn't fit into Chain B before you even try to run the code, you can read about LiteChain typing [here](https://rogeriochaves.github.io/litechain/docs/chain-basics/type_signatures). +The type signatures are an important part of LangStream, having them can save a lot of time preventing bugs and debugging issues caused for example when Stream B is not expecting the output of Stream A. Using an editor like VSCode with PyLance allows you to get warned that Stream A doesn't fit into Stream B before you even try to run the code, you can read about LangStream typing [here](https://rogeriochaves.github.io/langstream/docs/stream-basics/type_signatures). -Last but not least, you may also have noticed that both the emojis and the translation got printed in the final output, this is by design. In LiteChain, you always have access to everything that has gone through the whole chain in the final stream, this means that debugging it is very trivial, and a [`debug`](https://rogeriochaves.github.io/litechain/reference/litechain/index.html#litechain.debug) function is available to make it even easier. A property `output.final : bool` [is available](https://rogeriochaves.github.io/litechain/reference/litechain/index.html#litechain.ChainOutput.final) to be checked if you want to print just the results of the final Chain, but there are also more utility functions available to help you work with output stream as you wish, check out more about it on our [Why Streams? guide](https://rogeriochaves.github.io/litechain/docs/chain-basics/why_streams) and [the reference](https://rogeriochaves.github.io/litechain/reference/litechain/index.html). +Last but not least, you may also have noticed that both the emojis and the translation got printed in the final output, this is by design. In LangStream, you always have access to everything that has gone through the whole stream in the final stream, this means that debugging it is very trivial, and a [`debug`](https://rogeriochaves.github.io/langstream/reference/langstream/index.html#langstream.debug) function is available to make it even easier. A property `output.final : bool` [is available](https://rogeriochaves.github.io/langstream/reference/langstream/index.html#langstream.StreamOutput.final) to be checked if you want to print just the results of the final Stream, but there are also more utility functions available to help you work with output stream as you wish, check out more about it on our [Why Streams? guide](https://rogeriochaves.github.io/langstream/docs/stream-basics/why_streams) and [the reference](https://rogeriochaves.github.io/langstream/reference/langstream/index.html). # Prompts on the outside -In our experience, when working with LLM applications, the main part you must spend tunning are your prompts, which are not always portable if you switch LLMs. The content one chain produces might change a lot how another chain should be written, the prompt carry the personality and the goal of your app, doing good prompt engineering can really make it or break it. +In our experience, when working with LLM applications, the main part you must spend tunning are your prompts, which are not always portable if you switch LLMs. The content one stream produces might change a lot how another stream should be written, the prompt carry the personality and the goal of your app, doing good prompt engineering can really make it or break it. -That's why LiteChain does not hide prompts away in agents, we will give examples in the documentation, but believe you should build your own agents, to be able to customize them and their prompts later. LiteChain simply wants to facilitate and standardize the piping and connection between different parts, so you can focus on what is really important, we don't want you to spend time with LiteChain itself. +That's why LangStream does not hide prompts away in agents, we will give examples in the documentation, but believe you should build your own agents, to be able to customize them and their prompts later. LangStream simply wants to facilitate and standardize the piping and connection between different parts, so you can focus on what is really important, we don't want you to spend time with LangStream itself. # Bring your own integration -In addition, as the name implies, LiteChain wants to stay light, not embrace the world, the goal is that you really understand the Chain, making it very easy for your to add your own integration, without any additional layers in between. +In addition, as the name implies, LangStream wants to stay light, not embrace the world, the goal is that you really understand the Stream, making it very easy for your to add your own integration, without any additional layers in between. In our experience, wrappers can hurt more than they help, because instead of using the library or API you want to connect directly, now you need to learn another layer of indirection, which might not accept the same parameters to work the way you expect, it gets in the way. @@ -104,7 +104,7 @@ We do provide some integrations for OpenAI and GPT4All for example, but then we # 📖 Learn more -To continue developing with LiteChain, take a look at our [documentation](https://rogeriochaves.github.io/litechain) so you can find: +To continue developing with LangStream, take a look at our [documentation](https://rogeriochaves.github.io/langstream) so you can find: - Getting started - Detailed guides @@ -113,7 +113,7 @@ To continue developing with LiteChain, take a look at our [documentation](https: # 👥 Community -[Join our discord](https://discord.gg/48ZM5KkKgw) community to connect with other LiteChain developers, ask questions, get support, and stay updated with the latest news and announcements. +[Join our discord](https://discord.gg/48ZM5KkKgw) community to connect with other LangStream developers, ask questions, get support, and stay updated with the latest news and announcements. [![Join our Discord community](https://img.shields.io/badge/Join-Discord-7289DA.svg)](https://discord.gg/AmEMWmFG) @@ -125,7 +125,7 @@ To continue developing with LiteChain, take a look at our [documentation](https: # 🙋 Contributing -As a very new project in a rapidly developing field LiteChain is extremely open to contributions, we need a lot of help with integrations, documentation and guides content, feel free to send MRs and open issues. The project is very easy to run (check out the Makefile, it's all you need), but more complete contibuting guidelines to be written (we need help with that too!) +As a very new project in a rapidly developing field LangStream is extremely open to contributions, we need a lot of help with integrations, documentation and guides content, feel free to send MRs and open issues. The project is very easy to run (check out the Makefile, it's all you need), but more complete contibuting guidelines to be written (we need help with that too!) If you want to help me pay the bills and keep developing this project, you can: diff --git a/docs/docs/chain-basics/composing_chains.md b/docs/docs/chain-basics/composing_chains.md deleted file mode 100644 index e771fd6..0000000 --- a/docs/docs/chain-basics/composing_chains.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Composing Chains - -If you are familiar with Functional Programming, the Chain follows the [Monad Laws](https://wiki.haskell.org/Monad_laws), this ensures they are composable to build complex application following the Category Theory definitions. Our goal on building LiteChain was always to make it truly composable, and this is the best abstraction we know for the job, so we adopted it. - -But you don't need to understand any Functional Programming or fancy terms, just to understand the seven basic composition functions below: - -## `map()` - -This is the simplest one, the [`map()`](pathname:///reference/litechain/index.html#litechain.Chain.map) function transforms the output of a Chain, one token at a time as they arrive. The [`map()`](pathname:///reference/litechain/index.html#litechain.Chain.map) function is non-blocking, since it's processing the outputs as they come, so you shouldn't do heavy processing on it, although you can return asynchronous operations from it to await later. - -Here is an example: - -```python -from litechain import Chain, as_async_generator, join_final_output -import asyncio - -async def example(): - # produces one word at a time - words_chain = Chain[str, str]( - "WordsChain", lambda sentence: as_async_generator(*sentence.split(" ")) - ) - - # uppercases each word and take the first letter - # highlight-next-line - accronym_chain = words_chain.map(lambda word: word.upper()[0]) - - return await join_final_output(accronym_chain("as soon as possible")) - -asyncio.run(example()) -#=> 'ASAP' -``` - -As you can see, the words "as", "soon", "as" and "possible" are generated one at a time, then the `map()` function makes them uppercase and take the first letter, we join the final output later, resulting in ASAP. - -Here we are using a basic [`Chain`](pathname:///reference/litechain/index.html#chain), but try to replace it with an [`OpenAICompletionChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAICompletionChain) for example and you will see that the `map()` function and all other composition functions work just the same. - -## `and_then()` - -The [`and_then()`](pathname:///reference/litechain/index.html#litechain.Chain.and_then) is the true composition function, it's what -allows you to compose two chains together, taking the output of one chain, and using as input for another one. Since generally we want the first chain to be finished to send the input to the next one, for example for building a prompt, the [`and_then()`](pathname:///reference/litechain/index.html#litechain.Chain.and_then) function is blocking, which means it will wait for all tokens -to arrive from Chain A, collect them to a list, and only then call the Chain B. - -For example: - -```python -from litechain import Chain, as_async_generator, join_final_output -from typing import Iterable -import asyncio - -async def example(): - words_chain = Chain[str, str]( - "WordsChain", lambda sentence: as_async_generator(*sentence.split(" ")) - ) - - last_word_chain = Chain[Iterable[str], str]("LastWordChain", lambda words: list(words)[-1]) - - # highlight-next-line - chain = words_chain.and_then(last_word_chain) - - return await join_final_output(chain("This is time well spent. DUNE!")) - -asyncio.run(example()) -#=> 'DUNE!' -``` - -In this example, `last_word_chain` is a chain that takes only the last word that was generated, it takes an `Iterable[str]` as input and produces `str` (the last word) as output. There is no way for it to predict the last word, so of course it has to wait for the previous chain to finish, and `and_then()` does that. - -Also, not always the argument to `and_then()` must be another chain, in this case it's simple enough that it can just be a lambda: - -```python -composed_chain = words_chain.and_then(lambda words: list(words)[-1]) -``` - -Then again, it could also be an LLM producing tokens in place of those chains, try it out with an [`OpenAICompletionChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAICompletionChain). - -## `filter()` - -This is also a very simple one, the [`filter()`](pathname:///reference/litechain/index.html#litechain.Chain.map) function keeps the output values that return `True` for your test function. It it also non-blocking, dropping values from the strem as they arrive. For example: - -```python -from litechain import Chain, as_async_generator, collect_final_output -import asyncio - -async def example(): - numbers_chain = Chain[int, int]("NumbersChain", lambda input: as_async_generator(*range(0, input))) - even_chain = numbers_chain.filter(lambda input: input % 2 == 0) - return await collect_final_output(even_chain(9)) - -asyncio.run(example()) -#=> [0, 2, 4, 6, 8] -``` - -## `collect()` - -The [`collect()`](pathname:///reference/litechain/index.html#litechain.Chain.collect) function blocks a Chain until all the values have been generated, and collects it into a list, kinda like what `and_then()` does under the hood, but it doesn't take another chain as an argument, it takes no arguments, it just blocks the current chain transforming it into from a stream of items, to a single list item. - -You can use `collect()` + `map()` to achieve the same as the `and_then()` example above: - -```python -from litechain import Chain, as_async_generator, join_final_output -import asyncio - -async def example(): - words_chain = Chain[str, str]( - "WordsChain", lambda sentence: as_async_generator(*sentence.split(" ")) - ) - - # highlight-next-line - chain = words_chain.collect().map(lambda words: list(words)[-1]) - - return await join_final_output(chain("This is time well spent. DUNE!")) - -asyncio.run(example()) -#=> 'DUNE!' -``` - -## `join()` - -As you may have noticed, both `and_then()` and `collect()` produces a list of items from the previous chain output, this is because chains may produce any type of values, and a list is universal. However, for LLMs, the most common case is for them to produce `str`, which we want to join together as a final `str`, for that, you can use the [`join()`](pathname:///reference/litechain/index.html#litechain.Chain.join) function. - -The `join()` function is also blocking, and it will only work if you chain is producing `str` as output, otherwise it will show you a typing error. - -Here is an example: - -```python -from litechain import Chain, as_async_generator, join_final_output -import asyncio - -async def example(): - pairings_chain = Chain[None, str]( - "PairingsChain", lambda _: as_async_generator("Human ", "and ", "dog") - ) - - # highlight-start - chain = pairings_chain.join().map( - lambda pairing: "BEST FRIENDS!" if pairing == "Human and dog" else "meh" - ) - # highlight-end - - return await join_final_output(chain(None)) - -asyncio.run(example()) -#=> 'BEST FRIENDS!' -``` - -It is common practice to `join()` an LLM output before injecting it as another LLM input. - -## `gather()` - -Now, for the more advanced use case. Sometimes you want to call not one, but many LLMs at the same time in parallel, for example if you have a series of documents and you want to summarize and score them all, at the same time, to later decide which one is the best document. To create multiple processings, you can use `map()`, but then to wait on them all to finish, you have to use [`gather()`](pathname:///reference/litechain/index.html#litechain.Chain.gather). - -The [`gather()`](pathname:///reference/litechain/index.html#litechain.Chain.gather) function works similarly to [`asyncio.gather`](https://docs.python.org/3/library/asyncio-task.html#asyncio.gather), but instead of async functions, it can be executed on a chain that is generating other `AsyncGenerator`s (a chain of chains), it will process all those async generators at the same time in parallel and block until they all finish, then it will produce a `List` of `List`s with all the results. - -For example: - -```python -from litechain import Chain, as_async_generator, collect_final_output -from typing import AsyncGenerator -import asyncio - -async def delayed_output(x) -> AsyncGenerator[str, None]: - await asyncio.sleep(1) - yield f"Number: {x}" - -async def example(): - number_chain = Chain[int, int]( - "NumberChain", lambda x: as_async_generator(*range(x)) - ) - gathered_chain : Chain[int, str] = ( - number_chain.map(delayed_output) - .gather() - .and_then(lambda results: as_async_generator(*(r[0] for r in results))) - ) - return await collect_final_output(gathered_chain(1)) - -asyncio.run(example()) # will take 1s to finish, not 3s, because it runs in parallel -#=> ['Number: 0', 'Number: 1', 'Number: 2'] -``` - -In this simple example, we generate a range of numbers `[0, 1, 2]`, then for each of those, we simulate a heavy process that would take 1s to finish the `delayed_output`, we `map()` each number to this `delayed_output` function, which is a function that produces an `AsyncGenerator`, then we `gather()`, and then we take the first item of each. - -Because we used `gather()`, the chain will take `1s` to finish, because even though each one of the three numbers alone take `1s`, they are ran in parallel, so they finish all together. - -## `pipe()` - -The [`pipe()`](pathname:///reference/litechain/index.html#litechain.Chain.pipe) gives you a more lower-level composition, it actually gives you the underlying `AsyncGenerator` stream and expects that you -return another `AsyncGenerator` from there, the advantage of that is that you have really fine control, you can for example have something that is blocking and non-blocking at the same time: - -```python -from litechain import Chain, as_async_generator, collect_final_output -from typing import List, AsyncGenerator -import asyncio - -async def example(items): - async def mario_pipe(stream: AsyncGenerator[str, None]) -> AsyncGenerator[str, None]: - waiting_for_mushroom = False - async for item in stream: - if item == "Mario": - waiting_for_mushroom = True - elif item == "Mushroom" and waiting_for_mushroom: - yield "Super Mario!" - else: - yield item + "?" - - piped_chain = Chain[List[str], str]( - "PipedChain", lambda items: as_async_generator(*items) - ).pipe(mario_pipe) - - return await collect_final_output(piped_chain(items)) - -asyncio.run(example(["Mario", "Mushroom"])) -#=> ['Super Mario!'] - -asyncio.run(example(["Luigi"])) -#=> ['Luigi?'] - -asyncio.run(example(["Mario", "Luigi", "Mushroom"])) -#=> ['Luigi?', 'Super Mario!'] -``` - -As you can see this pipe blocks kinda like `and_then` when it sees "Mario", until a mushroom arrives, but for other random items -such as "Luigi" it just re-yields it immediately, adding a question mark, non-blocking, like `map`. In fact, you can use just -`pipe` to reconstruct `map`, `filter` and `and_then`! - -You can also call another chain from `pipe` directly, just be sure to re-yield its outputs - -## Standard nomenclature - -Now that you know the basic composing functions, it's also interesting to note everything in LiteChain also follow the same patterns, for example, for the final output we have the utilities [`filter_final_output()`](pathname:///reference/litechain/index.html#litechain.filter_final_output), [`collect_final_output()`](pathname:///reference/litechain/index.html#litechain.collect_final_output) and [`join_final_output()`](pathname:///reference/litechain/index.html#litechain.join_final_output), you can see they are using the same `filter`, `collect` and `join` names, and they work as you would expect them to. - -Now, that you know how to transform and compose chains, keep on reading to understand why type signatures are important to LiteChain. diff --git a/docs/docs/chain-basics/custom_chains.md b/docs/docs/chain-basics/custom_chains.md deleted file mode 100644 index eab9f3c..0000000 --- a/docs/docs/chain-basics/custom_chains.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Custom Chains - -If you have been following the guides, now you know how to create chains, how to compose them together, and everything, however, what if you want to change the core behaviour of the chain, how do you do it? Well, turns out, **there are no "custom chains" really**, it's all just composition. - -For example, let's say you want a chain that retries on error, using the [`@retry`](https://pypi.org/project/retry/) library annotation, you can simply create a function that wraps the chain to be retried: - -```python -from litechain import Chain -from retry import retry -from typing import TypeVar - -T = TypeVar("T") -U = TypeVar("U") - -def retriable(chain: Chain[T, U]) -> Chain[T, U]: - @retry(tries=3) - def call_wrapped_chain(input: T): - return chain(input) - - return Chain[T, U]("RetriableChain", call_wrapped_chain) -``` - -And use it like this: - -```python -from litechain import collect_final_output - -attempts = 0 - -def division_by_attempts(input: int): - global attempts - attempts += 1 - return input / (attempts - 1) - -chain = retriable( - Chain[int, float]("BrokenChain", division_by_attempts) -).map(lambda x: x + 1) - -await collect_final_output(chain(25)) -#=> [26] -``` - -This chain will first divide by zero, causing a `ZeroDivisionError`, but thanks to our little `retriable` wrapper, it will try again an succeed next time, returning `26`. - -So that's it, because chains are just input and output, a simple function will do, if you want to write a class to fit more the type system and be more pythonic, you also can, and the only method you need to override is `__init__`: - -```python -class RetriableChain(Chain[T, U]): - def __init__(self, chain: Chain[T, U], tries=3): - @retry(tries=tries) - def call_wrapped_chain(input: T): - return chain(input) - - super().__init__("RetriableChain", call_wrapped_chain) -``` - -This will work exactly the same as the function: - -```python -attempts = 0 - -chain = RetriableChain( - Chain[int, float]("BrokenChain", division_by_attempts) -).map(lambda x: x + 1) - -await collect_final_output(chain(25)) -#=> [26] -``` - -As a proof that this is enough, take a look at [how the OpenAICompletionChain is implemented](https://github.com/rogeriochaves/litechain/blob/main/litechain/contrib/llms/open_ai.py#L26), it's a simple wrapper of OpenAI's API under `__init__` and that's it. - -## Next Steps - -This concludes the guides for Chain Basics, congratulations! On the next steps, we are going to build some real application with real LLMs, stay tuned! diff --git a/docs/docs/chain-basics/index.md b/docs/docs/chain-basics/index.md deleted file mode 100644 index be47a41..0000000 --- a/docs/docs/chain-basics/index.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Chain Basics - -The [Chain](pathname:///reference/litechain/index.html#chain) is the main building block of LiteChain, you compose chains together to build your LLM application. - -A Chain is basically a function that takes an input and produces an [`AsyncGenerator`](https://peps.python.org/pep-0525/) of an output, if you are not familiar with async generators, you can think about it as a stream, or in other word, a list over time. - -The simplest of all chains, takes one input and produces a stream of outputs of a single item, and this is how you create one: - -```python -uppercase_chain = Chain[str, str]("UppercaseChain", lambda input: input.upper()) -``` - -As you can see, there are some parameters you pass to it, first of all is the type signature `[str, str]`, this defines the input and output types of the chain, respectively. In this case they are the same, but they could be different, you can read more about why types are important for LiteChain [here](/docs/chain-basics/type_signatures). - -It also takes a name, `"UppercaseChain"`, the reason for having a name is making it easier to debug, so it can be anything you want, as long as it's helpful for you to identify later. If any issues arrive along the way, you can debug and visualize exactly which chains are misbehaving. - -Then, the heart of the chain, is the lambda function that is executed when the chain is called. It takes exactly one input (which is `str` in this) and must return a value of the specified output type (also `str`), here it just returns the same input but in uppercase. - -Now that we have a chain, we can just run it, as a function, and we will get back an [`AsyncGenerator`](https://peps.python.org/pep-0525/) of outputs that we can iterate on. Here is the full example: - -```python -from litechain import Chain -import asyncio - -async def example(): - uppercase_chain = Chain[str, str]("UppercaseChain", lambda input: input.upper()) - - async for output in uppercase_chain("i am not screaming"): - print(output.data) - -asyncio.run(example()) -#=> I AM NOT SCREAMING -``` - -As you can see, upon calling the chain, we had to iterate over it using `async for`, this loop will only run once, because our chain is producing a single value, but still, it is necessary since chains are always producing async generators. -Go to the next section to understand better why is that. \ No newline at end of file diff --git a/docs/docs/chain-basics/type_signatures.md b/docs/docs/chain-basics/type_signatures.md deleted file mode 100644 index a2553c1..0000000 --- a/docs/docs/chain-basics/type_signatures.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Type Signatures - -In the recent years, Python has been expanding the support for [type hints](https://docs.python.org/3/library/typing.html), which helps a lot during development to catch bugs from types that should not be there, and even detecting `None`s before they happen. - -On quick scripts, notebooks, some web apps and throwaway code types might not be very useful and actually get in the way, however, when you are doing a lot of data piping and connecting many different pieces together, types are extremely useful, to help you save time and patience in debugging why two things are not working well together, and this is exactly what LiteChain is about. - -Because of that, LiteChain has a very thorough type annotation, with the goal of making it very reliable for the developer, but it works best when you explicitly declare the input and output types you expect when defining a chain: - -```python -len_chain = Chain[str, int]("LenChain", lambda input: len(input)) -``` - -This chain above for example, just counts the length of a string, so it takes a `str` and return an `int`. The nice side-effect of this is that you can see, at a glance, what goes in and what goes out of the chain, without needing to read it's implementation - -Now, Let's say you have this other chain: - -```python -happy_chain = Chain[str, str]("HappyChain", lambda input: input + " :)") -``` - -If you try to fit the two, it won't work, but instead of knowing that only when you run the code, you can find out instantly, if you use a code editor like VS Code (with PyLance extension for python type checking), it will look like this: - -![vscode showing typecheck error](/img/type-error-1.png) - -It says that `"Iterable[int]" is incompatible with "str"`, right, the first chain produces `int`s, so let's transform them to string by adding a `.map(str)`: - -![vscode showing typecheck error](/img/type-error-2.png) - -It still doesn't typecheck, but now it says that `"Iterable[str]" is incompatible with "str"`, of course, because chains produce a stream of things, not just one thing, and the `happy_chain` expect a single string, so this type error reminded us that we need to use `join()` first: - -![vscode showing no type errors](/img/type-error-3.png) - -Solved, no type errors now, with instant feedback. - -You don't necessarily need to add the types of your chains in LiteChain, it can be inferenced from the lambda or function you pass, however, be aware it may end up being not what you want, for example: - -```python -first_item_chain = Chain("FirstItemChain", lambda input: input[0]) - -await collect_final_output(first_item_chain([1, 2, 3])) -#=> [1] - -await collect_final_output(first_item_chain("Foo Bar")) -#=> ["F"] -``` - -The first `first_item_chain` was intended to take the first item only from lists, but it also works with strings actually, since `[0]` works on strings. This might not be what you expected at first, and lead to annoying bugs. If, however, you are explicit about your types, then the second call will show a type error, helping you to notice this early on, maybe before you run it: - -```python -first_item_chain = Chain[List[int], int]("FirstItemChain", lambda input: input[0]) - -await collect_final_output(first_item_chain([1, 2, 3])) -#=> [1] - -# highlight-next-line -await collect_final_output(first_item_chain("Foo Bar")) # ❌ type error -#=> ["F"] -``` - -Now types cannot prevent all errors, some will still happen at runtime. Go to the next page to check out on error handling. \ No newline at end of file diff --git a/docs/docs/chain-basics/working_with_streams.md b/docs/docs/chain-basics/working_with_streams.md deleted file mode 100644 index accbb58..0000000 --- a/docs/docs/chain-basics/working_with_streams.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Working with Streams - -By default, all LLMs generate a stream of tokens: - -```python -from litechain.contrib import OpenAICompletionChain - -bacon_chain = OpenAICompletionChain[str, str]( - "BaconChain", - lambda input: input, - model="ada", -) - -async for output in bacon_chain("I like bacon and"): - print(output.data) -#=> iced -#=> tea -#=> . -#=> I -#=> like -#=> to -#=> eat -#=> bacon -#=> and -#=> -#=> iced -#=> tea -#=> . -``` - -You can notice that it will print more or less one word per line, those are the tokens it is generating, since Python by default adds a new line for each `print` statement, we end up with one token per line. - -When creating a simple Chain, if you return a single value, it will also output just that single value, so if you want to simulate an LLM, and create a chain that produces a stream of outputs, you can use the [`as_async_generator()`](pathname:///reference/litechain/index.html#litechain.as_async_generator) utility function: - - -```python -from litechain import Chain, as_async_generator - -stream_of_bacon_chain = Chain[None, str]( - "StreamOfBaconChain", - lambda _: as_async_generator("I", "like", "bacon"), -) - -async for output in stream_of_bacon_chain(None): - print(output.data) -#=> I -#=> like -#=> bacon -``` - -## The whole Chain is streamed - -On LiteChain, when you compose two or more chains, map the results or apply any operations on it, still the original values of anything generating outputs anywhere in the chain gets streamed, this means that if you have a chain being mapped, -both the original output and the transformed ones will be outputted, for example: - -```python -from litechain import Chain, as_async_generator - -stream_of_bacon_chain = Chain[None, str]( - "StreamOfBaconChain", - lambda _: as_async_generator("I", "like", "bacon"), -) - -tell_the_world = stream_of_bacon_chain.map(lambda token: token.upper()) - -async for output in tell_the_world(None): - print(output.chain, ":", output.data) -#=> StreamOfBaconChain : I -#=> StreamOfBaconChain@map : I -#=> StreamOfBaconChain : like -#=> StreamOfBaconChain@map : LIKE -#=> StreamOfBaconChain : bacon -#=> StreamOfBaconChain@map : BACON -``` - -This is done by design so that you can always inspect what is going in the middle of a complex chain, either to debug it, or to display to the user for a better user experience. - -If you want just the final output, you can check for the property [`output.final`](pathname:///reference/litechain/index.html#litechain.ChainOutput.final): - -```python -import time - -async for output in tell_the_world(None): - if output.final: - time.sleep(1) # added for dramatic effect - print(output.data) -#=> I -#=> LIKE -#=> BACON -``` - -## Output Utils - -Now, as shown on the examples, you need to iterate over it with `async for` to get the final output. However, you might not care about streaming or inspecting the middle results at all, and just want the final result as a whole. For that, you can use some utility functions that LiteChain provides, for example, [`collect_final_output()`](pathname:///reference/litechain/index.html#litechain.collect_final_output), which gives you a single list with the final outputs all at once: - -```python -from litechain import collect_final_output - -await collect_final_output(tell_the_world(None)) -#=> ['I', 'LIKE', 'BACON'] -``` - -Or, if you chain's final output is `str`, then you can use [`join_final_output()`](pathname:///reference/litechain/index.html#litechain.join_final_output), which gives you already the full string, concatenated - -```python -from litechain import join_final_output - -await join_final_output(tell_the_world(None)) -#=> 'ILIKEBACON' -``` - -(LLMs produce spaces as token as well, so normally the lack of spaces in here is not a problem) - -Check out also [`filter_final_output()`](pathname:///reference/litechain/index.html#litechain.filter_final_output), which gives you still an `AsyncGenerator` to loop over, but including only the final results. - -Now that you know all about streams, you need to understand what does that mean when you are composing them together, keep on reading to learn about Composing Chains. \ No newline at end of file diff --git a/docs/docs/examples/index.md b/docs/docs/examples/index.md index 8ef37d1..325843c 100644 --- a/docs/docs/examples/index.md +++ b/docs/docs/examples/index.md @@ -4,7 +4,7 @@ sidebar_position: 5 # Code Examples -In this section you will find a list of code examples on what you can build with LiteChain. Due to the focus of LiteChain to keep the core light, we prefer to provide extensive examples of what you can do rather than extensive pre-made classes that do it for you, this makes it easier for you to understand how everything is working and connected together, and simply adapt to your use-case. +In this section you will find a list of code examples on what you can build with LangStream. Due to the focus of LangStream to keep the core light, we prefer to provide extensive examples of what you can do rather than extensive pre-made classes that do it for you, this makes it easier for you to understand how everything is working and connected together, and simply adapt to your use-case. import DocCardList from '@theme/DocCardList'; diff --git a/docs/docs/examples/openai-function-call-extract-schema.ipynb b/docs/docs/examples/openai-function-call-extract-schema.ipynb index ea70162..e37f413 100644 --- a/docs/docs/examples/openai-function-call-extract-schema.ipynb +++ b/docs/docs/examples/openai-function-call-extract-schema.ipynb @@ -12,7 +12,7 @@ "\n", "# Extracting Schema for OpenAI Functions\n", "\n", - "In the code example below, we use the [openai_function_call](https://github.com/jxnl/openai_function_call) library to extract a schema to be used on [`OpenAIChatChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatChain) from a good old python function, so you don't need to write the schema yourself.\n", + "In the code example below, we use the [openai_function_call](https://github.com/jxnl/openai_function_call) library to extract a schema to be used on [`OpenAIChatStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatStream) from a good old python function, so you don't need to write the schema yourself.\n", "\n", "First you need to install the library:\n", "\n", @@ -91,7 +91,7 @@ "id": "012951b1", "metadata": {}, "source": [ - "We can then use this schema directly on our chain:" + "We can then use this schema directly on our stream:" ] }, { @@ -115,13 +115,13 @@ "import json\n", "from typing import Union\n", "\n", - "from litechain import Chain, collect_final_output\n", - "from litechain.contrib import OpenAIChatChain, OpenAIChatDelta, OpenAIChatMessage\n", + "from langstream import Stream, collect_final_output\n", + "from langstream.contrib import OpenAIChatStream, OpenAIChatDelta, OpenAIChatMessage\n", "\n", - "chain: Chain[str, Union[OpenAIChatDelta, WeatherReturn]] = OpenAIChatChain[\n", + "stream: Stream[str, Union[OpenAIChatDelta, WeatherReturn]] = OpenAIChatStream[\n", " str, OpenAIChatDelta\n", "](\n", - " \"WeatherChain\",\n", + " \"WeatherStream\",\n", " lambda user_input: [\n", " OpenAIChatMessage(role=\"user\", content=user_input),\n", " ],\n", @@ -136,7 +136,7 @@ ")\n", "\n", "await collect_final_output(\n", - " chain(\n", + " stream(\n", " \"I'm in my appartment in Amsterdam, thinking... should I take an umbrella for my pet chicken?\"\n", " )\n", ")" diff --git a/docs/docs/examples/qa-over-documents.ipynb b/docs/docs/examples/qa-over-documents.ipynb index 1b61dbe..05903fb 100644 --- a/docs/docs/examples/qa-over-documents.ipynb +++ b/docs/docs/examples/qa-over-documents.ipynb @@ -12,7 +12,7 @@ "\n", "A common use case for LLMs is to reply answers to users based on search on a knowledge base, generally using a vector db under the hood.\n", "\n", - "LiteChain does not provide an agent to do that out-of-the-box, however, it is very easy to build one yourself, processing the files and then building a chain to do the search and question answering.\n", + "LangStream does not provide an agent to do that out-of-the-box, however, it is very easy to build one yourself, processing the files and then building a stream to do the search and question answering.\n", "\n", "The goal of this guide is for you to understand every step, so you are able to modify it according to your needs later on." ] @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Before being able to interact with your docs, first you need to extract all embeddings from it, and index in a vector db, this will allow the LLMs to quickly retrieve and then answer questions on the matching documents. So let's get to the setup, this part has no involvement of LiteChain at all.\n", + "Before being able to interact with your docs, first you need to extract all embeddings from it, and index in a vector db, this will allow the LLMs to quickly retrieve and then answer questions on the matching documents. So let's get to the setup, this part has no involvement of LangStream at all.\n", "\n", "First, let's use the [`unstructured`](https://github.com/Unstructured-IO/unstructured) library to parse all the markdown files in our docs folder:" ] @@ -44,13 +44,13 @@ "text": [ "Parsing docs/docs/intro.md...\n", "Parsing docs/docs/ui/chainlit.md...\n", - "Parsing docs/docs/chain-basics/why_streams.md...\n", - "Parsing docs/docs/chain-basics/error_handling.md...\n", - "Parsing docs/docs/chain-basics/custom_chains.md...\n", - "Parsing docs/docs/chain-basics/working_with_streams.md...\n", - "Parsing docs/docs/chain-basics/index.md...\n", - "Parsing docs/docs/chain-basics/type_signatures.md...\n", - "Parsing docs/docs/chain-basics/composing_chains.md...\n", + "Parsing docs/docs/stream-basics/why_streams.md...\n", + "Parsing docs/docs/stream-basics/error_handling.md...\n", + "Parsing docs/docs/stream-basics/custom_streams.md...\n", + "Parsing docs/docs/stream-basics/working_with_streams.md...\n", + "Parsing docs/docs/stream-basics/index.md...\n", + "Parsing docs/docs/stream-basics/type_signatures.md...\n", + "Parsing docs/docs/stream-basics/composing_streams.md...\n", "Parsing docs/docs/examples/weather-bot.md...\n", "Parsing docs/docs/examples/index.md...\n", "Parsing docs/docs/examples/openai-function-call-extract-schema.md...\n", @@ -180,14 +180,14 @@ "LLMs require a lot of GPU to run properly make it hard for the common folk to set one up locally. Fortunately, the folks at GPT4All are doing an excellent job in reall...\n", "\n", "LLMs\n", - "Large Language Models like GPT-4 is the whole reason LiteChain exists, we want to build on top of LLMs to construct an application. After learning the Chain Basics, it should ...\n", + "Large Language Models like GPT-4 is the whole reason LangStream exists, we want to build on top of LLMs to construct an application. After learning the Stream Basics, it should ...\n", "\n" ] } ], "source": [ "from typing import AsyncGenerator\n", - "from litechain import as_async_generator\n", + "from langstream import as_async_generator\n", "\n", "def retrieve_documents(query: str, n_results: int) -> AsyncGenerator[str, None]:\n", " query_embedding = (\n", @@ -218,7 +218,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, with all our documents indexed and our query function, we can now write a chain that will reply the user questions about those documents using an LLM:" + "Finally, with all our documents indexed and our query function, we can now write a stream that will reply the user questions about those documents using an LLM:" ] }, { @@ -230,51 +230,51 @@ "name": "stdout", "output_type": "stream", "text": [ - "To combine two chains in LiteChain, you can use the `and_then()` function. This function allows you to compose two chains together by taking the output of one chain and using it as the input for another chain.\n", + "To combine two streams in LangStream, you can use the `and_then()` function. This function allows you to compose two streams together by taking the output of one stream and using it as the input for another stream.\n", "\n", - "Here's an example of how to combine two chains using `and_then()`:\n", + "Here's an example of how to combine two streams using `and_then()`:\n", "\n", "```python\n", - "from litechain import Chain\n", + "from langstream import Stream\n", "\n", - "# Define the first chain\n", - "chain1 = Chain[str, str](\"Chain1\", lambda input: input.upper())\n", + "# Define the first stream\n", + "stream1 = Stream[str, str](\"Stream1\", lambda input: input.upper())\n", "\n", - "# Define the second chain\n", - "chain2 = Chain[str, str](\"Chain2\", lambda input: input + \"!\")\n", + "# Define the second stream\n", + "stream2 = Stream[str, str](\"Stream2\", lambda input: input + \"!\")\n", "\n", - "# Combine the two chains\n", - "combined_chain = chain1.and_then(chain2)\n", + "# Combine the two streams\n", + "combined_stream = stream1.and_then(stream2)\n", "\n", - "# Run the combined chain\n", - "output = combined_chain(\"hello\")\n", + "# Run the combined stream\n", + "output = combined_stream(\"hello\")\n", "print(output) # Output: \"HELLO!\"\n", "```\n", "\n", - "In this example, `chain1` takes a string as input and converts it to uppercase. `chain2` takes a string as input and adds an exclamation mark at the end. The `and_then()` function combines `chain1` and `chain2` together, so the output of `chain1` is passed as input to `chain2`. Finally, we run the combined chain with the input \"hello\" and get the output \"HELLO!\".\n", + "In this example, `stream1` takes a string as input and converts it to uppercase. `stream2` takes a string as input and adds an exclamation mark at the end. The `and_then()` function combines `stream1` and `stream2` together, so the output of `stream1` is passed as input to `stream2`. Finally, we run the combined stream with the input \"hello\" and get the output \"HELLO!\".\n", "\n", - "You can chain together as many chains as you need using `and_then()`. Each chain will receive the output of the previous chain as its input." + "You can stream together as many streams as you need using `and_then()`. Each stream will receive the output of the previous stream as its input." ] } ], "source": [ "from typing import Iterable\n", - "from litechain import Chain, filter_final_output\n", - "from litechain.contrib import OpenAIChatChain, OpenAIChatMessage, OpenAIChatDelta\n", + "from langstream import Stream, filter_final_output\n", + "from langstream.contrib import OpenAIChatStream, OpenAIChatMessage, OpenAIChatDelta\n", "\n", "\n", - "def chain(query):\n", + "def stream(query):\n", " return (\n", - " Chain[str, str](\n", - " \"RetrieveDocumentsChain\",\n", + " Stream[str, str](\n", + " \"RetrieveDocumentsStream\",\n", " lambda query: retrieve_documents(query, n_results=3),\n", " ).and_then(\n", - " OpenAIChatChain[Iterable[str], OpenAIChatDelta](\n", - " \"AnswerChain\",\n", + " OpenAIChatStream[Iterable[str], OpenAIChatDelta](\n", + " \"AnswerStream\",\n", " lambda results: [\n", " OpenAIChatMessage(\n", " role=\"system\",\n", - " content=\"You are a helpful bot that helps users answering questions about documentation of the LiteChain library\",\n", + " content=\"You are a helpful bot that helps users answering questions about documentation of the LangStream library\",\n", " ),\n", " OpenAIChatMessage(role=\"user\", content=query),\n", " OpenAIChatMessage(\n", @@ -289,7 +289,7 @@ " )(query)\n", "\n", "\n", - "async for output in filter_final_output(chain(\"How do I combine two chains?\")):\n", + "async for output in filter_final_output(stream(\"How do I combine two streams?\")):\n", " print(output.content, end=\"\")" ] }, @@ -315,18 +315,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "To add memory to your LiteChain library, you can follow these steps:\n", + "To add memory to your LangStream library, you can follow these steps:\n", "\n", "1. Define a memory variable: This can be a list, dictionary, or any other data structure that suits your needs. For example, you can define a list called `memory` to store previous chat messages.\n", "\n", "2. Create a function to save messages to memory: This function should take a message as input and append it to the memory variable. For example, you can create a function called `save_message_to_memory` that appends the message to the `memory` list.\n", "\n", - "3. Update the chain to include memory operations: Depending on your use case, you may need to update your chain to include memory operations. This can be done using the `map` function to apply the memory operations to each message in the chain. For example, you can use the `map` function to call the `save_message_to_memory` function for each message in the chain.\n", + "3. Update the stream to include memory operations: Depending on your use case, you may need to update your stream to include memory operations. This can be done using the `map` function to apply the memory operations to each message in the stream. For example, you can use the `map` function to call the `save_message_to_memory` function for each message in the stream.\n", "\n", - "Here is an example code snippet that demonstrates how to add memory to a LiteChain library:\n", + "Here is an example code snippet that demonstrates how to add memory to a LangStream library:\n", "\n", "```python\n", - "from litechain import Chain\n", + "from langstream import Stream\n", "\n", "# Define the memory variable\n", "memory = []\n", @@ -336,18 +336,18 @@ " memory.append(message)\n", " return message\n", "\n", - "# Create the chain\n", - "chain = Chain().map(save_message_to_memory)\n", + "# Create the stream\n", + "stream = Stream().map(save_message_to_memory)\n", "\n", - "# Use the chain\n", - "output = chain(\"Hello, world!\")\n", + "# Use the stream\n", + "output = stream(\"Hello, world!\")\n", "print(output)\n", "\n", "# Access the memory\n", "print(memory)\n", "```\n", "\n", - "In this example, the `save_message_to_memory` function appends each message to the `memory` list. The `map` function is used to apply the `save_message_to_memory` function to each message in the chain. Finally, the `memory` variable can be accessed to retrieve the stored messages.\n", + "In this example, the `save_message_to_memory` function appends each message to the `memory` list. The `map` function is used to apply the `save_message_to_memory` function to each message in the stream. Finally, the `memory` variable can be accessed to retrieve the stored messages.\n", "\n", "Remember to customize the code according to your specific requirements and use case." ] @@ -356,12 +356,12 @@ "source": [ "import json\n", "from typing import Iterable, List, Tuple\n", - "from litechain import Chain, filter_final_output\n", - "from litechain.contrib import OpenAIChatChain, OpenAIChatMessage, OpenAIChatDelta\n", + "from langstream import Stream, filter_final_output\n", + "from langstream.contrib import OpenAIChatStream, OpenAIChatMessage, OpenAIChatDelta\n", "from openai_function_call import openai_function\n", "\n", "\n", - "def chain(query):\n", + "def stream(query):\n", " @openai_function\n", " def score_document(score: int) -> int:\n", " \"\"\"\n", @@ -375,8 +375,8 @@ "\n", " return score\n", "\n", - " scoring_chain: Chain[str, int] = OpenAIChatChain[str, OpenAIChatDelta](\n", - " \"AnswerChain\",\n", + " scoring_stream: Stream[str, int] = OpenAIChatStream[str, OpenAIChatDelta](\n", + " \"AnswerStream\",\n", " lambda document: [\n", " OpenAIChatMessage(\n", " role=\"system\",\n", @@ -405,14 +405,14 @@ " return document\n", "\n", " return (\n", - " Chain[str, str](\n", - " \"RetrieveDocumentsChain\",\n", + " Stream[str, str](\n", + " \"RetrieveDocumentsStream\",\n", " lambda query: retrieve_documents(query, n_results=10),\n", " )\n", " # Store retrieved documents to be used later\n", " .map(append_document)\n", - " # Score each of them using another chain\n", - " .map(scoring_chain)\n", + " # Score each of them using another stream\n", + " .map(scoring_stream)\n", " # Run it all in parallel\n", " .gather()\n", " # Pair up documents with scores\n", @@ -423,12 +423,12 @@ " )\n", " # Now use them to build an answer\n", " .and_then(\n", - " OpenAIChatChain[Iterable[List[Tuple[str, int]]], OpenAIChatDelta](\n", - " \"AnswerChain\",\n", + " OpenAIChatStream[Iterable[List[Tuple[str, int]]], OpenAIChatDelta](\n", + " \"AnswerStream\",\n", " lambda results: [\n", " OpenAIChatMessage(\n", " role=\"system\",\n", - " content=\"You are a helpful bot that helps users answering questions about documentation of the LiteChain library\",\n", + " content=\"You are a helpful bot that helps users answering questions about documentation of the LangStream library\",\n", " ),\n", " OpenAIChatMessage(role=\"user\", content=query),\n", " OpenAIChatMessage(\n", @@ -442,7 +442,7 @@ " )(query)\n", "\n", "\n", - "async for output in filter_final_output(chain(\"How do I add memory?\")):\n", + "async for output in filter_final_output(stream(\"How do I add memory?\")):\n", " print(output.content, end=\"\")" ] }, diff --git a/docs/docs/examples/serve-with-fastapi.ipynb b/docs/docs/examples/serve-with-fastapi.ipynb index bdbf0cf..83e1724 100644 --- a/docs/docs/examples/serve-with-fastapi.ipynb +++ b/docs/docs/examples/serve-with-fastapi.ipynb @@ -28,10 +28,10 @@ "source": [ "import json\n", "from typing import AsyncGenerator, List, Literal\n", - "from litechain import Chain, debug, as_async_generator\n", + "from langstream import Stream, debug, as_async_generator\n", "\n", - "from litechain.contrib.llms.open_ai import (\n", - " OpenAIChatChain,\n", + "from langstream.contrib.llms.open_ai import (\n", + " OpenAIChatStream,\n", " OpenAIChatDelta,\n", " OpenAIChatMessage,\n", ")\n", @@ -73,13 +73,13 @@ " )\n", "\n", "\n", - "# Chain Definitions\n", + "# Stream Definitions\n", "\n", "\n", "def weather_bot(memory):\n", - " weather_chain = (\n", - " OpenAIChatChain[str, OpenAIChatDelta](\n", - " \"WeatherChain\",\n", + " weather_stream = (\n", + " OpenAIChatStream[str, OpenAIChatDelta](\n", + " \"WeatherStream\",\n", " lambda user_input: [\n", " *memory.history,\n", " memory.save_message(\n", @@ -119,8 +119,8 @@ " .map(memory.update_delta)\n", " )\n", "\n", - " function_reply_chain = OpenAIChatChain[None, OpenAIChatDelta](\n", - " \"FunctionReplyChain\",\n", + " function_reply_stream = OpenAIChatStream[None, OpenAIChatDelta](\n", + " \"FunctionReplyStream\",\n", " lambda _: memory.history,\n", " model=\"gpt-3.5-turbo-0613\",\n", " temperature=0,\n", @@ -129,13 +129,13 @@ " async def reply_function_call(stream: AsyncGenerator[OpenAIChatDelta, None]):\n", " async for output in stream:\n", " if output.role == \"function\":\n", - " async for output in function_reply_chain(None):\n", + " async for output in function_reply_stream(None):\n", " yield output\n", " else:\n", " yield output\n", "\n", - " weather_bot: Chain[str, str] = (\n", - " weather_chain.pipe(reply_function_call)\n", + " weather_bot: Stream[str, str] = (\n", + " weather_stream.pipe(reply_function_call)\n", " .map(memory.update_delta)\n", " .map(lambda delta: delta.content)\n", " )\n", @@ -161,7 +161,7 @@ "from typing import Dict\n", "from fastapi import FastAPI, Request\n", "from fastapi.responses import StreamingResponse\n", - "from litechain import filter_final_output\n", + "from langstream import filter_final_output\n", "\n", "app = FastAPI()\n", "\n", diff --git a/docs/docs/examples/weather-bot-error-handling.ipynb b/docs/docs/examples/weather-bot-error-handling.ipynb index c2e3fdb..76b95d8 100644 --- a/docs/docs/examples/weather-bot-error-handling.ipynb +++ b/docs/docs/examples/weather-bot-error-handling.ipynb @@ -24,14 +24,14 @@ "source": [ "import json\n", "from typing import Any, AsyncGenerator, List, Literal, Tuple, TypedDict\n", - "from litechain import debug, as_async_generator\n", + "from langstream import debug, as_async_generator\n", "\n", - "from litechain.contrib.llms.open_ai import (\n", - " OpenAIChatChain,\n", + "from langstream.contrib.llms.open_ai import (\n", + " OpenAIChatStream,\n", " OpenAIChatDelta,\n", " OpenAIChatMessage,\n", ")\n", - "from litechain.core.chain import Chain, ChainOutput\n", + "from langstream.core.stream import Stream, StreamOutput\n", "\n", "\n", "class Memory(TypedDict):\n", @@ -75,21 +75,21 @@ "\n", "def error_handler(\n", " err: Exception,\n", - ") -> AsyncGenerator[ChainOutput[OpenAIChatDelta], Any]:\n", + ") -> AsyncGenerator[StreamOutput[OpenAIChatDelta], Any]:\n", " # Try to recover from the error if it happened on the function calling\n", " if \"get_current_weather\" in str(err):\n", - " x = function_error_chain((\"get_current_weather\", err))\n", + " x = function_error_stream((\"get_current_weather\", err))\n", " return x\n", " else:\n", " # Otherwise just re-raise it\n", " raise err\n", "\n", "\n", - "# Chain Definitions\n", + "# Stream Definitions\n", "\n", - "weather_chain = debug(\n", - " OpenAIChatChain[str, OpenAIChatDelta](\n", - " \"WeatherChain\",\n", + "weather_stream = debug(\n", + " OpenAIChatStream[str, OpenAIChatDelta](\n", + " \"WeatherStream\",\n", " lambda user_input: [\n", " OpenAIChatMessage(\n", " role=\"system\",\n", @@ -147,19 +147,19 @@ " .map(update_delta_on_memory)\n", ")\n", "\n", - "function_reply_chain = debug(\n", - " OpenAIChatChain[None, OpenAIChatDelta](\n", - " \"FunctionReplyChain\",\n", + "function_reply_stream = debug(\n", + " OpenAIChatStream[None, OpenAIChatDelta](\n", + " \"FunctionReplyStream\",\n", " lambda _: memory[\"history\"],\n", " model=\"gpt-3.5-turbo-0613\",\n", " temperature=0,\n", " ).map(update_delta_on_memory)\n", ")\n", "\n", - "# If an error happens, this chain is triggered, it simply takes the current history, plus a user message with the error message\n", + "# If an error happens, this stream is triggered, it simply takes the current history, plus a user message with the error message\n", "# this is enough for the model to figure out what was the issue and ask user for additional input\n", - "function_error_chain = OpenAIChatChain[Tuple[str, Exception], OpenAIChatDelta](\n", - " \"FunctionErrorChain\",\n", + "function_error_stream = OpenAIChatStream[Tuple[str, Exception], OpenAIChatDelta](\n", + " \"FunctionErrorStream\",\n", " lambda name_and_err: [\n", " *memory[\"history\"],\n", " save_message_to_memory(\n", @@ -173,9 +173,9 @@ " temperature=0,\n", ")\n", "\n", - "weather_bot: Chain[str, OpenAIChatDelta] = weather_chain.and_then(\n", + "weather_bot: Stream[str, OpenAIChatDelta] = weather_stream.and_then(\n", " # Reply based on function result if last output was a function output\n", - " lambda outputs: function_reply_chain(None)\n", + " lambda outputs: function_reply_stream(None)\n", " if list(outputs)[-1].role == \"function\"\n", " # Otherwise just re-yield the outputs\n", " else as_async_generator(*outputs)\n", @@ -194,14 +194,14 @@ "text": [ "\n", "\n", - "\u001b[32m> WeatherChain\u001b[39m\n", + "\u001b[32m> WeatherStream\u001b[39m\n", "\n", "\u001b[33mAssistant:\u001b[39m Hello! How can I assist you today?" ] } ], "source": [ - "from litechain.utils.chain import collect_final_output\n", + "from langstream.utils.stream import collect_final_output\n", "\n", "_ = await collect_final_output(weather_bot(\"hi there\"))" ] @@ -218,22 +218,22 @@ "text": [ "\n", "\n", - "\u001b[32m> WeatherChain\u001b[39m\n", + "\u001b[32m> WeatherStream\u001b[39m\n", "\n", "\u001b[33mFunction get_current_weather:\u001b[39m {}\n", "\n", - "\u001b[32m> WeatherChain@map\u001b[39m\n", + "\u001b[32m> WeatherStream@map\u001b[39m\n", "\n", "\u001b[31mException:\u001b[39m get_current_weather() missing 1 required positional argument: 'location'\n", "\n", - "\u001b[32m> FunctionErrorChain\u001b[39m\n", + "\u001b[32m> FunctionErrorStream\u001b[39m\n", "\n", "\u001b[33mAssistant:\u001b[39m I apologize for the inconvenience. In order to provide you with the current weather, could you please provide me with your location?" ] } ], "source": [ - "from litechain.utils.chain import collect_final_output\n", + "from langstream.utils.stream import collect_final_output\n", "\n", "_ = await collect_final_output(weather_bot(\"it is hot today?\"))" ] @@ -250,17 +250,17 @@ "text": [ "\n", "\n", - "\u001b[32m> WeatherChain\u001b[39m\n", + "\u001b[32m> WeatherStream\u001b[39m\n", "\n", "\u001b[33mFunction get_current_weather:\u001b[39m {\n", " \"location\": \"Amsterdam\"\n", "}\n", "\n", - "\u001b[32m> WeatherChain@map\u001b[39m\n", + "\u001b[32m> WeatherStream@map\u001b[39m\n", "\n", "\u001b[33mFunction get_current_weather:\u001b[39m {\"location\": \"Amsterdam\", \"forecast\": \"sunny\", \"temperature\": \"25 C\"}\n", "\n", - "\u001b[32m> FunctionReplyChain\u001b[39m\n", + "\u001b[32m> FunctionReplyStream\u001b[39m\n", "\n", "\u001b[33mAssistant:\u001b[39m It seems that the current weather in Amsterdam is sunny with a temperature of 25°C. Stay hydrated and enjoy the day!" ] @@ -276,7 +276,7 @@ "id": "79f25374", "metadata": {}, "source": [ - "As you can see, the bot first tried to call `get_current_weather` with empty arguments, which threw an error, we inject this error back into the `FunctionErrorChain`, making the bot realize the mistake and ask the user to provide the location. Once provided, the function call is triggered again, this time with the right location and response.\n", + "As you can see, the bot first tried to call `get_current_weather` with empty arguments, which threw an error, we inject this error back into the `FunctionErrorStream`, making the bot realize the mistake and ask the user to provide the location. Once provided, the function call is triggered again, this time with the right location and response.\n", "\n", "Now take a look on what happened inside the memory, we save both the original function call and the error message there:\n" ] diff --git a/docs/docs/examples/weather-bot.ipynb b/docs/docs/examples/weather-bot.ipynb index 84eccdf..8cdd619 100644 --- a/docs/docs/examples/weather-bot.ipynb +++ b/docs/docs/examples/weather-bot.ipynb @@ -24,10 +24,10 @@ "source": [ "import json\n", "from typing import List, Literal, TypedDict\n", - "from litechain import Chain, debug, as_async_generator\n", + "from langstream import Stream, debug, as_async_generator\n", "\n", - "from litechain.contrib.llms.open_ai import (\n", - " OpenAIChatChain,\n", + "from langstream.contrib.llms.open_ai import (\n", + " OpenAIChatStream,\n", " OpenAIChatDelta,\n", " OpenAIChatMessage,\n", ")\n", @@ -69,12 +69,12 @@ " )\n", "\n", "\n", - "# Chain Definitions\n", + "# Stream Definitions\n", "\n", - "weather_chain = (\n", + "weather_stream = (\n", " debug(\n", - " OpenAIChatChain[str, OpenAIChatDelta](\n", - " \"WeatherChain\",\n", + " OpenAIChatStream[str, OpenAIChatDelta](\n", + " \"WeatherStream\",\n", " lambda user_input: [\n", " *memory[\"history\"],\n", " save_message_to_memory(\n", @@ -115,18 +115,18 @@ " .map(update_delta_on_memory)\n", ")\n", "\n", - "function_reply_chain = debug(\n", - " OpenAIChatChain[None, OpenAIChatDelta](\n", - " \"FunctionReplyChain\",\n", + "function_reply_stream = debug(\n", + " OpenAIChatStream[None, OpenAIChatDelta](\n", + " \"FunctionReplyStream\",\n", " lambda _: memory[\"history\"],\n", " model=\"gpt-3.5-turbo-0613\",\n", " temperature=0,\n", " )\n", ").map(update_delta_on_memory)\n", "\n", - "weather_bot: Chain[str, OpenAIChatDelta] = weather_chain.and_then(\n", + "weather_bot: Stream[str, OpenAIChatDelta] = weather_stream.and_then(\n", " # Reply based on function result if last output was a function output\n", - " lambda outputs: function_reply_chain(None)\n", + " lambda outputs: function_reply_stream(None)\n", " if list(outputs)[-1].role == \"function\"\n", " # Otherwise just re-yield the outputs\n", " else as_async_generator(*outputs)\n", @@ -145,14 +145,14 @@ "text": [ "\n", "\n", - "\u001b[32m> WeatherChain\u001b[39m\n", + "\u001b[32m> WeatherStream\u001b[39m\n", "\n", "\u001b[33mAssistant:\u001b[39m Hello! How can I assist you today?" ] } ], "source": [ - "from litechain.utils.chain import collect_final_output\n", + "from langstream.utils.stream import collect_final_output\n", "\n", "_ = await collect_final_output(weather_bot(\"hi there\"))" ] @@ -169,13 +169,13 @@ "text": [ "\n", "\n", - "\u001b[32m> WeatherChain\u001b[39m\n", + "\u001b[32m> WeatherStream\u001b[39m\n", "\n", "\u001b[33mFunction get_current_weather:\u001b[39m {\n", " \"location\": \"Amsterdam\"\n", "}\n", "\n", - "\u001b[32m> FunctionReplyChain\u001b[39m\n", + "\u001b[32m> FunctionReplyStream\u001b[39m\n", "\n", "\u001b[33mAssistant:\u001b[39m Yes, it is hot today in Amsterdam. The current temperature is 25°C and it is sunny." ] diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 3fe9c8a..bb688c1 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -5,19 +5,19 @@ title: Getting Started # Introduction -LiteChain is a lighter alternative to LangChain for building LLMs application, instead of having a massive amount of features and classes, LiteChain focuses on having a single small core, that is easy to learn, easy to adapt, well documented, fully typed and truly composable. +LangStream is a lighter alternative to LangChain for building LLMs application, instead of having a massive amount of features and classes, LangStream focuses on having a single small core, that is easy to learn, easy to adapt, well documented, fully typed and truly composable, with Streams instead of chains as the building block. -LiteChain also puts emphasis on "explicit is better than implicit", which means less magic and a bit more legwork, but on the other hand, you will be able to understand everything that is going on in between, making your application easy to maintain and customize. +LangStream also puts emphasis on "explicit is better than implicit", which means less magic and a bit more legwork, but on the other hand, you will be able to understand everything that is going on in between, making your application easy to maintain and customize. ## Getting Started You can install it with pip: ``` -pip install litechain +pip install langstream ``` -## Your First Chain +## Your First Stream To run this example, first you will need to get an [API key from OpenAI](https://platform.openai.com), then export it with: @@ -25,17 +25,17 @@ To run this example, first you will need to get an [API key from OpenAI](https:/ export OPENAI_API_KEY= ``` -(if you really cannot get access to the API, you can try [GPT4All](pathname:///reference/litechain/contrib/index.html#litechain.contrib.GPT4AllChain) instead, it's completely free and runs locally) +(if you really cannot get access to the API, you can try [GPT4All](pathname:///reference/langstream/contrib/index.html#langstream.contrib.GPT4AllStream) instead, it's completely free and runs locally) Now create a new file `main.py` and paste this example: ```python -from litechain.contrib import OpenAIChatChain, OpenAIChatMessage, OpenAIChatDelta +from langstream.contrib import OpenAIChatStream, OpenAIChatMessage, OpenAIChatDelta import asyncio -# Creating a GPT-3.5 EmojiChain -emoji_chain = OpenAIChatChain[str, OpenAIChatDelta]( - "EmojiChain", +# Creating a GPT-3.5 EmojiStream +emoji_stream = OpenAIChatStream[str, OpenAIChatDelta]( + "EmojiStream", lambda user_message: [ OpenAIChatMessage( role="user", content=f"{user_message}. Reply in emojis" @@ -48,7 +48,7 @@ emoji_chain = OpenAIChatChain[str, OpenAIChatDelta]( async def main(): while True: print("> ", end="") - async for output in emoji_chain(input()): + async for output in emoji_stream(input()): print(output.data.content, end="") print("") @@ -65,4 +65,4 @@ This will create a basic chat on the terminal, and for any questions you ask the ## Next Steps -Continue on reading to learn the Chain basics, we will then build up on more complex examples, can't wait! +Continue on reading to learn the Stream basics, we will then build up on more complex examples, can't wait! diff --git a/docs/docs/llms/gpt4all.md b/docs/docs/llms/gpt4all.md index dfa5f26..1844d9b 100644 --- a/docs/docs/llms/gpt4all.md +++ b/docs/docs/llms/gpt4all.md @@ -4,24 +4,24 @@ sidebar_position: 4 # GPT4All LLMs -LLMs require a lot of GPU to run properly make it hard for the common folk to set one up locally. Fortunately, the folks at [GPT4All](https://gpt4all.io/index.html) are doing an excellent job in really reducing those models with various techniques, and speeding them up to run on CPUs everywhere with no issues. LiteChain also provides a thin wrapper for them, and since it's local, no API keys are required. +LLMs require a lot of GPU to run properly make it hard for the common folk to set one up locally. Fortunately, the folks at [GPT4All](https://gpt4all.io/index.html) are doing an excellent job in really reducing those models with various techniques, and speeding them up to run on CPUs everywhere with no issues. LangStream also provides a thin wrapper for them, and since it's local, no API keys are required. -# GPT4AllChain +# GPT4AllStream -You can use a [`GPT4AllChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.GPT4AllChain) like this: +You can use a [`GPT4AllStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.GPT4AllStream) like this: ```python -from litechain import join_final_output -from litechain.contrib import GPT4AllChain +from langstream import join_final_output +from langstream.contrib import GPT4AllStream -greet_chain = GPT4AllChain[str, str]( - "GreetingChain", +greet_stream = GPT4AllStream[str, str]( + "GreetingStream", lambda name: f"### User: Hello little person that lives in my CPU, my name is {name}. How is it going?\\n\\n### Response:", model="orca-mini-3b.ggmlv3.q4_0.bin", temperature=0, ) -await join_final_output(greet_chain("Alice")) +await join_final_output(greet_stream("Alice")) #=> " I'm doing well, thank you for asking! How about you?" ``` @@ -29,4 +29,4 @@ The first time you run it, it will download the model you are using (in this cas Then, you might have noticed the prompt is just a string, but we do have roles markers inside it, with `### User:` and `### Response:`, with two `\n\n` line breaks in between. This is how GPT4All models were trained, and you can use this same patterns to keep the roles behaviour. -Also, in the example we used `temperature=0`, for stability as explained [here](/docs/llms/zero_temperature), but [`GPT4AllChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.GPT4AllChain) has many more parameters you can adjust that can work better depending on the model you are choosing, check them out on [the reference](pathname:///reference/litechain/contrib/index.html#litechain.contrib.GPT4AllChain). \ No newline at end of file +Also, in the example we used `temperature=0`, for stability as explained [here](/docs/llms/zero_temperature), but [`GPT4AllStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.GPT4AllStream) has many more parameters you can adjust that can work better depending on the model you are choosing, check them out on [the reference](pathname:///reference/langstream/contrib/index.html#langstream.contrib.GPT4AllStream). \ No newline at end of file diff --git a/docs/docs/llms/index.md b/docs/docs/llms/index.md index 53ca946..7a0315e 100644 --- a/docs/docs/llms/index.md +++ b/docs/docs/llms/index.md @@ -4,6 +4,6 @@ sidebar_position: 3 # LLMs -Large Language Models like GPT-4 is the whole reason LiteChain exists, we want to build on top of LLMs to construct an application. After learning the [Chain Basics](/docs/chain-basics), it should be clear how you can wrap any LLM in a [`Chain`](pathname:///reference/litechain/index.html#chain), you just need to produce an [`AsyncGenerator`](https://peps.python.org/pep-0525/) out of their output. However, LiteChain already come with some LLM chains out of the box to make it easier. +Large Language Models like GPT-4 is the whole reason LangStream exists, we want to build on top of LLMs to construct an application. After learning the [Stream Basics](/docs/stream-basics), it should be clear how you can wrap any LLM in a [`Stream`](pathname:///reference/langstream/index.html#stream), you just need to produce an [`AsyncGenerator`](https://peps.python.org/pep-0525/) out of their output. However, LangStream already come with some LLM streams out of the box to make it easier. -Like other things that are not part of the core of the library, they live under `litechain.contrib`. Go ahead for OpenAI examples. \ No newline at end of file +Like other things that are not part of the core of the library, they live under `langstream.contrib`. Go ahead for OpenAI examples. \ No newline at end of file diff --git a/docs/docs/llms/memory.md b/docs/docs/llms/memory.md index 8c84db2..e201bdb 100644 --- a/docs/docs/llms/memory.md +++ b/docs/docs/llms/memory.md @@ -4,17 +4,17 @@ sidebar_position: 5 # Adding Memory -LLMs are stateless, and LiteChain also strive to be as stateless as possible, which makes things easier to reason about. However, this means your Chains will have no memory by default. +LLMs are stateless, and LangStream also strive to be as stateless as possible, which makes things easier to reason about. However, this means your Streams will have no memory by default. -Following the "explicit is better than implicit" philosophy, in LiteChain you manage the memory yourself, so you are in full control of what is stored to memory and where it is used and when. +Following the "explicit is better than implicit" philosophy, in LangStream you manage the memory yourself, so you are in full control of what is stored to memory and where it is used and when. ## Simple Text Completion Memory The memory can be as simple as a variable, like this [GPT4All](gpt4all) chatbot with memory: ```python -from litechain import Chain, join_final_output -from litechain.contrib import GPT4AllChain +from langstream import Stream, join_final_output +from langstream.contrib import GPT4AllStream from textwrap import dedent memory = "" @@ -24,8 +24,8 @@ def save_to_memory(str: str) -> str: memory += str return str -magical_numbers_bot: Chain[str, str] = GPT4AllChain[str, str]( - "MagicalNumbersChain", +magical_numbers_bot: Stream[str, str] = GPT4AllStream[str, str]( + "MagicalNumbersStream", lambda user_message: memory + save_to_memory(f"""\n @@ -50,17 +50,17 @@ The way this memory implementation works is very simple, we have a string to hol Then, we have a `save_to_memory` function, which just takes any string, appends it to the `memory` variable, and return the same string back, we use it in two places: when creating the prompt, to be able to save the user input to memory, and then on the `map(save_to_memory)` function, which appends each generated token to memory as they come. -Try adding some `print(memory)` statements before and after each chain call to see how memory is changing. +Try adding some `print(memory)` statements before and after each stream call to see how memory is changing. ## OpenAI Chat Memory -Adding memory to OpenAI Chat is a bit more tricky, because it takes a list of [`OpenAIChatMessage`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatMessage)s for the prompt, and generates [`OpenAIChatDelta`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatMessage)s as output. +Adding memory to OpenAI Chat is a bit more tricky, because it takes a list of [`OpenAIChatMessage`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatMessage)s for the prompt, and generates [`OpenAIChatDelta`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatMessage)s as output. This means that we cannot use a simple string as memory, but we can use a simple list. Also now we need a function to update the last message on the memory with the incoming delta for each update, like this: ```python -from litechain import Chain, join_final_output -from litechain.contrib import OpenAIChatMessage, OpenAIChatDelta, OpenAIChatChain +from langstream import Stream, join_final_output +from langstream.contrib import OpenAIChatMessage, OpenAIChatDelta, OpenAIChatStream from typing import List @@ -80,9 +80,9 @@ def update_delta_on_memory(delta: OpenAIChatDelta) -> OpenAIChatDelta: return delta -chain: Chain[str, str] = ( - OpenAIChatChain[str, OpenAIChatDelta]( - "EmojiChatChain", +stream: Stream[str, str] = ( + OpenAIChatStream[str, OpenAIChatDelta]( + "EmojiChatStream", lambda user_message: [ *memory, save_message_to_memory( @@ -98,10 +98,10 @@ chain: Chain[str, str] = ( .map(lambda delta: delta.content) ) -await join_final_output(chain("Hey there, my name is 🧨 how is it going?")) +await join_final_output(stream("Hey there, my name is 🧨 how is it going?")) #=> '👋🧨😊' -await join_final_output(chain("What is my name?")) +await join_final_output(stream("What is my name?")) #=> '🤔❓🧨' ``` @@ -109,4 +109,4 @@ You can see that the LLM remembers your name, which is 🧨, a very common name In this example, we do a similar thing that we did on the first one, except that instead of concatenating the memory into the prompt string, we are expanding it into the prompt list with `*memory`. Also, we cannot use the same function on the `map` call, because we don't have full messages back, but deltas, which we need to use to update the last message on the memory, the function `update_delta_on_memory` takes care of that for us. -I hope this guide now made it more clear how can you have memory on your Chains. In future releases, LiteChain might release a more standard way of dealing with memory, but this is not the case yet, please join us in the discussion on how an official memory module should look like if you have ideas! \ No newline at end of file +I hope this guide now made it more clear how can you have memory on your Streams. In future releases, LangStream might release a more standard way of dealing with memory, but this is not the case yet, please join us in the discussion on how an official memory module should look like if you have ideas! \ No newline at end of file diff --git a/docs/docs/llms/open_ai.md b/docs/docs/llms/open_ai.md index 7f51e78..906b1ff 100644 --- a/docs/docs/llms/open_ai.md +++ b/docs/docs/llms/open_ai.md @@ -10,44 +10,44 @@ OpenAI took the world by storm with the launch of ChatGPT and GPT-4, at the poin export OPENAI_API_KEY= ``` -Then, LiteChain provides two thin wrapper layers for their APIs: +Then, LangStream provides two thin wrapper layers for their APIs: ## Text Completion OpenAI has two modes of generating text with LLMs, the first one, simple and more, is text completion, using GPT-3 derived models: ```python -from litechain import join_final_output -from litechain.contrib import OpenAICompletionChain +from langstream import join_final_output +from langstream.contrib import OpenAICompletionStream -recipe_chain = OpenAICompletionChain[str, str]( - "RecipeChain", +recipe_stream = OpenAICompletionStream[str, str]( + "RecipeStream", lambda recipe_name: f"Here is my {recipe_name} recipe: ", model="davinci", ) -await join_final_output(recipe_chain("instant noodles")) +await join_final_output(recipe_stream("instant noodles")) #=> '\xa01. Boil water 2. Add noodles 3. Add seasoning 4.' ``` This model is not specialized for chat as ChatGPT and GPT-4, but for completion, consider this while writing your prompts. In this case, we are taking the user input and making it part of a sentence for the model to complete ("Here is my..."), instead of asking a question for the model to answer. -When you use `OpenAICompletionChain` instead of a regular `Chain`, the lambda function you pass should return the prompt you want for the model consume, and the stream of outputs will be generated by the LLM, given this prompt. +When you use `OpenAICompletionStream` instead of a regular `Stream`, the lambda function you pass should return the prompt you want for the model consume, and the stream of outputs will be generated by the LLM, given this prompt. You also must specify which model to use, OpenAI has several variations of the GPT-3 text completion model, take a look at [their page](https://platform.openai.com/docs/models/gpt-3) to see which ones are available. -You also have other parameters you can pass to the chain like `temperature`, which [helps with development if you keep it at zero](/docs/llms/zero_temperature), and `max_tokens`, take a look at [the reference](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAICompletionChain) to learn more. +You also have other parameters you can pass to the stream like `temperature`, which [helps with development if you keep it at zero](/docs/llms/zero_temperature), and `max_tokens`, take a look at [the reference](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAICompletionStream) to learn more. ## Chat Completion -The most popular and powerful OpenAI completion API, however, is the Chat Completion, which gives you access to `gpt-3.5-turbo` and `gpt-4` models. It is a bit more work to work with, because it has defined roles, for `system`, `user`, `assistant` or `function`. You define an [`OpenAIChatChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatChain) like this: +The most popular and powerful OpenAI completion API, however, is the Chat Completion, which gives you access to `gpt-3.5-turbo` and `gpt-4` models. It is a bit more work to work with, because it has defined roles, for `system`, `user`, `assistant` or `function`. You define an [`OpenAIChatStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatStream) like this: ```python -from litechain import Chain, join_final_output -from litechain.contrib import OpenAIChatChain, OpenAIChatMessage, OpenAIChatDelta +from langstream import Stream, join_final_output +from langstream.contrib import OpenAIChatStream, OpenAIChatMessage, OpenAIChatDelta -recipe_chain: Chain[str, str] = OpenAIChatChain[str, OpenAIChatDelta]( - "RecipeChain", +recipe_stream: Stream[str, str] = OpenAIChatStream[str, OpenAIChatDelta]( + "RecipeStream", lambda recipe_name: [ OpenAIChatMessage( role="system", @@ -61,13 +61,13 @@ recipe_chain: Chain[str, str] = OpenAIChatChain[str, OpenAIChatDelta]( model="gpt-3.5-turbo", ).map(lambda delta: delta.content) -await join_final_output(recipe_chain("instant noodles")) +await join_final_output(recipe_stream("instant noodles")) #=> "Of course! Here's a simple and delicious recipe for instant noodles:\n\nIngredients:\n- 1 packet of instant noodles (your choice of flavor)\n- 2 cups of water\n- 1 tablespoon of vegetable oil\n- 1 small onion, thinly sliced\n- 1 clove of garlic, minced\n- 1 small carrot, julienned\n- 1/2 cup of sliced mushrooms\n- 1/2 cup of shredded cabbage\n- 2 tablespoons of soy sauce\n- 1 teaspoon of sesame oil\n- Optional toppings: sliced green onions, boiled egg, cooked chicken or shrimp, chili flakes\n\nInstructions:\n1. In a medium-sized pot, bring the water to a boil. Add the instant noodles and cook according to the package instructions until they are al dente. Drain and set aside.\n\n2. In the same pot, heat the vegetable oil over medium heat. Add the sliced onion and minced garlic, and sauté until they become fragrant and slightly caramelized.\n\n3. Add the julienned carrot, sliced mushrooms, and shredded cabbage to the pot. Stir-fry for a few minutes until the vegetables are slightly softened.\n\n4. Add the cooked instant noodles to the pot and toss them with the vegetables.\n\n5. In a small bowl, mix together the soy sauce and sesame oil. Pour this mixture over the noodles and vegetables, and toss everything together until well combined.\n\n6. Cook for an additional 2-3 minutes, stirring occasionally, to allow the flavors to meld together.\n\n7. Remove the pot from heat and divide the noodles into serving bowls. Top with your desired toppings such as sliced green onions, boiled egg, cooked chicken or shrimp, and chili flakes.\n\n8. Serve the instant noodles hot and enjoy!\n\nFeel free to customize this recipe by adding your favorite vegetables or protein. Enjoy your homemade instant noodles!" ``` -This model is really optimized for answering questions and following guidance. If you look at the lambda function, we do not return a simple string for the prompt, but a list of [`OpenAIChatMessage`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatMessage)s, those hold the role and the content. +This model is really optimized for answering questions and following guidance. If you look at the lambda function, we do not return a simple string for the prompt, but a list of [`OpenAIChatMessage`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatMessage)s, those hold the role and the content. -Then, if you look at the type signature of `OpenAIChatChain`, you will notice that it takes a `str` but it returns an [`OpenAIChatDelta`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatMessage), this is what OpenAI's chat completion API streams back to us, it holds also the `role` and the `content`, so before joining the chain, we need to do a `map` on the `delta.content` to get strings back. +Then, if you look at the type signature of `OpenAIChatStream`, you will notice that it takes a `str` but it returns an [`OpenAIChatDelta`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatMessage), this is what OpenAI's chat completion API streams back to us, it holds also the `role` and the `content`, so before joining the stream, we need to do a `map` on the `delta.content` to get strings back. Now, you just learned how to use OpenAI for text and chat completion, which is powerful enough, but there is one even more powerful feature, which we put on a separate guide, OpenAI Function Calling, check it out on the next guide. diff --git a/docs/docs/llms/open_ai_functions.md b/docs/docs/llms/open_ai_functions.md index 795da17..5987f08 100644 --- a/docs/docs/llms/open_ai_functions.md +++ b/docs/docs/llms/open_ai_functions.md @@ -8,7 +8,7 @@ By default, LLMs take text as input, and product text as output, but when we are So OpenAI developed a feature that constrains the logits to produce a valid structure[[1]](https://github.com/newhouseb/clownfish/), effectively getting them to create a valid schema for doing function calling, which enables us to get structured output and routing effortlessly. You can read more about it in their [official announcement](https://openai.com/blog/function-calling-and-other-api-updates). -To pass a function for [`OpenAIChatChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatChain) to call, simply pass the function schema in the `function` argument. For example, let's say you want the model to call this function to get the current weather: +To pass a function for [`OpenAIChatStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatStream) to call, simply pass the function schema in the `function` argument. For example, let's say you want the model to call this function to get the current weather: ```python from typing import TypedDict, Literal @@ -32,14 +32,14 @@ This function simply returns a mocked weather response using the `WeatherReturn` ```python from typing import Union -from litechain import Chain, collect_final_output -from litechain.contrib import OpenAIChatChain, OpenAIChatMessage, OpenAIChatDelta +from langstream import Stream, collect_final_output +from langstream.contrib import OpenAIChatStream, OpenAIChatMessage, OpenAIChatDelta import json -chain: Chain[str, Union[OpenAIChatDelta, WeatherReturn]] = OpenAIChatChain[ +stream: Stream[str, Union[OpenAIChatDelta, WeatherReturn]] = OpenAIChatStream[ str, OpenAIChatDelta ]( - "WeatherChain", + "WeatherStream", lambda user_input: [ OpenAIChatMessage(role="user", content=user_input), ], @@ -73,16 +73,16 @@ chain: Chain[str, Union[OpenAIChatDelta, WeatherReturn]] = OpenAIChatChain[ ) await collect_final_output( - chain( + stream( "I'm in my appartment in Amsterdam, thinking... should I take an umbrella for my pet chicken?" ) ) # [{'location': 'Amsterdam', 'forecast': 'sunny', 'temperature': '25 C'}] ``` -With the `functions` schema in place, we then map the deltas comming from `OpenAIChatChain`, and from there we call our actual function by decoding the json containing the arguments in case the delta is a function. +With the `functions` schema in place, we then map the deltas comming from `OpenAIChatStream`, and from there we call our actual function by decoding the json containing the arguments in case the delta is a function. -Notice how the output type of the chain becomes `Union[OpenAIChatDelta, WeatherReturn]`, this is because the chain now can return either a simple message reply, if the user says "hello, what's up" for example, or it may return a `WeatherReturn` because they user has asked about the weather and therefore we called the function. You could then wire this response to another LLM call to reply the user message for example. +Notice how the output type of the stream becomes `Union[OpenAIChatDelta, WeatherReturn]`, this is because the stream now can return either a simple message reply, if the user says "hello, what's up" for example, or it may return a `WeatherReturn` because they user has asked about the weather and therefore we called the function. You could then wire this response to another LLM call to reply the user message for example. As a tip, if you don't want to write the schema yourself, you can extract it from the function definition, check it out our [example on it](../examples/openai-function-call-extract-schema). diff --git a/docs/docs/llms/zero_temperature.md b/docs/docs/llms/zero_temperature.md index 168a6e2..835b8fa 100644 --- a/docs/docs/llms/zero_temperature.md +++ b/docs/docs/llms/zero_temperature.md @@ -10,4 +10,4 @@ When temperature is zero, it means the LLM will always choose the highest probab In a way, it follows the analogy with temperature in physics, when temperature is high, atoms get agitaded and moving all around, when temperature is low, they are more stable and move less, in more predictable manners. -We then recommend setting the temperature of your LLMs to zero for development, to be able to develop and test it with more ease, and once everything is in place, you can try to increase the temperature of some chains and retest to see if the results feel better. \ No newline at end of file +We then recommend setting the temperature of your LLMs to zero for development, to be able to develop and test it with more ease, and once everything is in place, you can try to increase the temperature of some streams and retest to see if the results feel better. \ No newline at end of file diff --git a/docs/docs/chain-basics/_category_.json b/docs/docs/stream-basics/_category_.json similarity index 100% rename from docs/docs/chain-basics/_category_.json rename to docs/docs/stream-basics/_category_.json diff --git a/docs/docs/stream-basics/composing_streams.md b/docs/docs/stream-basics/composing_streams.md new file mode 100644 index 0000000..cc7d5ee --- /dev/null +++ b/docs/docs/stream-basics/composing_streams.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 4 +--- + +# Composing Streams + +If you are familiar with Functional Programming, the Stream follows the [Monad Laws](https://wiki.haskell.org/Monad_laws), this ensures they are composable to build complex application following the Category Theory definitions. Our goal on building LangStream was always to make it truly composable, and this is the best abstraction we know for the job, so we adopted it. + +But you don't need to understand any Functional Programming or fancy terms, just to understand the seven basic composition functions below: + +## `map()` + +This is the simplest one, the [`map()`](pathname:///reference/langstream/index.html#langstream.Stream.map) function transforms the output of a Stream, one token at a time as they arrive. The [`map()`](pathname:///reference/langstream/index.html#langstream.Stream.map) function is non-blocking, since it's processing the outputs as they come, so you shouldn't do heavy processing on it, although you can return asynchronous operations from it to await later. + +Here is an example: + +```python +from langstream import Stream, as_async_generator, join_final_output +import asyncio + +async def example(): + # produces one word at a time + words_stream = Stream[str, str]( + "WordsStream", lambda sentence: as_async_generator(*sentence.split(" ")) + ) + + # uppercases each word and take the first letter + # highlight-next-line + accronym_stream = words_stream.map(lambda word: word.upper()[0]) + + return await join_final_output(accronym_stream("as soon as possible")) + +asyncio.run(example()) +#=> 'ASAP' +``` + +As you can see, the words "as", "soon", "as" and "possible" are generated one at a time, then the `map()` function makes them uppercase and take the first letter, we join the final output later, resulting in ASAP. + +Here we are using a basic [`Stream`](pathname:///reference/langstream/index.html#stream), but try to replace it with an [`OpenAICompletionStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAICompletionStream) for example and you will see that the `map()` function and all other composition functions work just the same. + +## `and_then()` + +The [`and_then()`](pathname:///reference/langstream/index.html#langstream.Stream.and_then) is the true composition function, it's what +allows you to compose two streams together, taking the output of one stream, and using as input for another one. Since generally we want the first stream to be finished to send the input to the next one, for example for building a prompt, the [`and_then()`](pathname:///reference/langstream/index.html#langstream.Stream.and_then) function is blocking, which means it will wait for all tokens +to arrive from Stream A, collect them to a list, and only then call the Stream B. + +For example: + +```python +from langstream import Stream, as_async_generator, join_final_output +from typing import Iterable +import asyncio + +async def example(): + words_stream = Stream[str, str]( + "WordsStream", lambda sentence: as_async_generator(*sentence.split(" ")) + ) + + last_word_stream = Stream[Iterable[str], str]("LastWordStream", lambda words: list(words)[-1]) + + # highlight-next-line + stream = words_stream.and_then(last_word_stream) + + return await join_final_output(stream("This is time well spent. DUNE!")) + +asyncio.run(example()) +#=> 'DUNE!' +``` + +In this example, `last_word_stream` is a stream that takes only the last word that was generated, it takes an `Iterable[str]` as input and produces `str` (the last word) as output. There is no way for it to predict the last word, so of course it has to wait for the previous stream to finish, and `and_then()` does that. + +Also, not always the argument to `and_then()` must be another stream, in this case it's simple enough that it can just be a lambda: + +```python +composed_stream = words_stream.and_then(lambda words: list(words)[-1]) +``` + +Then again, it could also be an LLM producing tokens in place of those streams, try it out with an [`OpenAICompletionStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAICompletionStream). + +## `filter()` + +This is also a very simple one, the [`filter()`](pathname:///reference/langstream/index.html#langstream.Stream.map) function keeps the output values that return `True` for your test function. It it also non-blocking, dropping values from the strem as they arrive. For example: + +```python +from langstream import Stream, as_async_generator, collect_final_output +import asyncio + +async def example(): + numbers_stream = Stream[int, int]("NumbersStream", lambda input: as_async_generator(*range(0, input))) + even_stream = numbers_stream.filter(lambda input: input % 2 == 0) + return await collect_final_output(even_stream(9)) + +asyncio.run(example()) +#=> [0, 2, 4, 6, 8] +``` + +## `collect()` + +The [`collect()`](pathname:///reference/langstream/index.html#langstream.Stream.collect) function blocks a Stream until all the values have been generated, and collects it into a list, kinda like what `and_then()` does under the hood, but it doesn't take another stream as an argument, it takes no arguments, it just blocks the current stream transforming it into from a stream of items, to a single list item. + +You can use `collect()` + `map()` to achieve the same as the `and_then()` example above: + +```python +from langstream import Stream, as_async_generator, join_final_output +import asyncio + +async def example(): + words_stream = Stream[str, str]( + "WordsStream", lambda sentence: as_async_generator(*sentence.split(" ")) + ) + + # highlight-next-line + stream = words_stream.collect().map(lambda words: list(words)[-1]) + + return await join_final_output(stream("This is time well spent. DUNE!")) + +asyncio.run(example()) +#=> 'DUNE!' +``` + +## `join()` + +As you may have noticed, both `and_then()` and `collect()` produces a list of items from the previous stream output, this is because streams may produce any type of values, and a list is universal. However, for LLMs, the most common case is for them to produce `str`, which we want to join together as a final `str`, for that, you can use the [`join()`](pathname:///reference/langstream/index.html#langstream.Stream.join) function. + +The `join()` function is also blocking, and it will only work if you stream is producing `str` as output, otherwise it will show you a typing error. + +Here is an example: + +```python +from langstream import Stream, as_async_generator, join_final_output +import asyncio + +async def example(): + pairings_stream = Stream[None, str]( + "PairingsStream", lambda _: as_async_generator("Human ", "and ", "dog") + ) + + # highlight-start + stream = pairings_stream.join().map( + lambda pairing: "BEST FRIENDS!" if pairing == "Human and dog" else "meh" + ) + # highlight-end + + return await join_final_output(stream(None)) + +asyncio.run(example()) +#=> 'BEST FRIENDS!' +``` + +It is common practice to `join()` an LLM output before injecting it as another LLM input. + +## `gather()` + +Now, for the more advanced use case. Sometimes you want to call not one, but many LLMs at the same time in parallel, for example if you have a series of documents and you want to summarize and score them all, at the same time, to later decide which one is the best document. To create multiple processings, you can use `map()`, but then to wait on them all to finish, you have to use [`gather()`](pathname:///reference/langstream/index.html#langstream.Stream.gather). + +The [`gather()`](pathname:///reference/langstream/index.html#langstream.Stream.gather) function works similarly to [`asyncio.gather`](https://docs.python.org/3/library/asyncio-task.html#asyncio.gather), but instead of async functions, it can be executed on a stream that is generating other `AsyncGenerator`s (a stream of streams), it will process all those async generators at the same time in parallel and block until they all finish, then it will produce a `List` of `List`s with all the results. + +For example: + +```python +from langstream import Stream, as_async_generator, collect_final_output +from typing import AsyncGenerator +import asyncio + +async def delayed_output(x) -> AsyncGenerator[str, None]: + await asyncio.sleep(1) + yield f"Number: {x}" + +async def example(): + number_stream = Stream[int, int]( + "NumberStream", lambda x: as_async_generator(*range(x)) + ) + gathered_stream : Stream[int, str] = ( + number_stream.map(delayed_output) + .gather() + .and_then(lambda results: as_async_generator(*(r[0] for r in results))) + ) + return await collect_final_output(gathered_stream(1)) + +asyncio.run(example()) # will take 1s to finish, not 3s, because it runs in parallel +#=> ['Number: 0', 'Number: 1', 'Number: 2'] +``` + +In this simple example, we generate a range of numbers `[0, 1, 2]`, then for each of those, we simulate a heavy process that would take 1s to finish the `delayed_output`, we `map()` each number to this `delayed_output` function, which is a function that produces an `AsyncGenerator`, then we `gather()`, and then we take the first item of each. + +Because we used `gather()`, the stream will take `1s` to finish, because even though each one of the three numbers alone take `1s`, they are ran in parallel, so they finish all together. + +## `pipe()` + +The [`pipe()`](pathname:///reference/langstream/index.html#langstream.Stream.pipe) gives you a more lower-level composition, it actually gives you the underlying `AsyncGenerator` stream and expects that you +return another `AsyncGenerator` from there, the advantage of that is that you have really fine control, you can for example have something that is blocking and non-blocking at the same time: + +```python +from langstream import Stream, as_async_generator, collect_final_output +from typing import List, AsyncGenerator +import asyncio + +async def example(items): + async def mario_pipe(stream: AsyncGenerator[str, None]) -> AsyncGenerator[str, None]: + waiting_for_mushroom = False + async for item in stream: + if item == "Mario": + waiting_for_mushroom = True + elif item == "Mushroom" and waiting_for_mushroom: + yield "Super Mario!" + else: + yield item + "?" + + piped_stream = Stream[List[str], str]( + "PipedStream", lambda items: as_async_generator(*items) + ).pipe(mario_pipe) + + return await collect_final_output(piped_stream(items)) + +asyncio.run(example(["Mario", "Mushroom"])) +#=> ['Super Mario!'] + +asyncio.run(example(["Luigi"])) +#=> ['Luigi?'] + +asyncio.run(example(["Mario", "Luigi", "Mushroom"])) +#=> ['Luigi?', 'Super Mario!'] +``` + +As you can see this pipe blocks kinda like `and_then` when it sees "Mario", until a mushroom arrives, but for other random items +such as "Luigi" it just re-yields it immediately, adding a question mark, non-blocking, like `map`. In fact, you can use just +`pipe` to reconstruct `map`, `filter` and `and_then`! + +You can also call another stream from `pipe` directly, just be sure to re-yield its outputs + +## Standard nomenclature + +Now that you know the basic composing functions, it's also interesting to note everything in LangStream also follow the same patterns, for example, for the final output we have the utilities [`filter_final_output()`](pathname:///reference/langstream/index.html#langstream.filter_final_output), [`collect_final_output()`](pathname:///reference/langstream/index.html#langstream.collect_final_output) and [`join_final_output()`](pathname:///reference/langstream/index.html#langstream.join_final_output), you can see they are using the same `filter`, `collect` and `join` names, and they work as you would expect them to. + +Now, that you know how to transform and compose streams, keep on reading to understand why type signatures are important to LangStream. diff --git a/docs/docs/stream-basics/custom_streams.md b/docs/docs/stream-basics/custom_streams.md new file mode 100644 index 0000000..91f612e --- /dev/null +++ b/docs/docs/stream-basics/custom_streams.md @@ -0,0 +1,78 @@ +--- +sidebar_position: 7 +--- + +# Custom Streams + +If you have been following the guides, now you know how to create streams, how to compose them together, and everything, however, what if you want to change the core behaviour of the stream, how do you do it? Well, turns out, **there are no "custom streams" really**, it's all just composition. + +For example, let's say you want a stream that retries on error, using the [`@retry`](https://pypi.org/project/retry/) library annotation, you can simply create a function that wraps the stream to be retried: + +```python +from langstream import Stream +from retry import retry +from typing import TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +def retriable(stream: Stream[T, U]) -> Stream[T, U]: + @retry(tries=3) + def call_wrapped_stream(input: T): + return stream(input) + + return Stream[T, U]("RetriableStream", call_wrapped_stream) +``` + +And use it like this: + +```python +from langstream import collect_final_output + +attempts = 0 + +def division_by_attempts(input: int): + global attempts + attempts += 1 + return input / (attempts - 1) + +stream = retriable( + Stream[int, float]("BrokenStream", division_by_attempts) +).map(lambda x: x + 1) + +await collect_final_output(stream(25)) +#=> [26] +``` + +This stream will first divide by zero, causing a `ZeroDivisionError`, but thanks to our little `retriable` wrapper, it will try again an succeed next time, returning `26`. + +So that's it, because streams are just input and output, a simple function will do, if you want to write a class to fit more the type system and be more pythonic, you also can, and the only method you need to override is `__init__`: + +```python +class RetriableStream(Stream[T, U]): + def __init__(self, stream: Stream[T, U], tries=3): + @retry(tries=tries) + def call_wrapped_stream(input: T): + return stream(input) + + super().__init__("RetriableStream", call_wrapped_stream) +``` + +This will work exactly the same as the function: + +```python +attempts = 0 + +stream = RetriableStream( + Stream[int, float]("BrokenStream", division_by_attempts) +).map(lambda x: x + 1) + +await collect_final_output(stream(25)) +#=> [26] +``` + +As a proof that this is enough, take a look at [how the OpenAICompletionStream is implemented](https://github.com/rogeriochaves/langstream/blob/main/langstream/contrib/llms/open_ai.py#L26), it's a simple wrapper of OpenAI's API under `__init__` and that's it. + +## Next Steps + +This concludes the guides for Stream Basics, congratulations! On the next steps, we are going to build some real application with real LLMs, stay tuned! diff --git a/docs/docs/chain-basics/error_handling.md b/docs/docs/stream-basics/error_handling.md similarity index 51% rename from docs/docs/chain-basics/error_handling.md rename to docs/docs/stream-basics/error_handling.md index f875391..4d451a0 100644 --- a/docs/docs/chain-basics/error_handling.md +++ b/docs/docs/stream-basics/error_handling.md @@ -6,34 +6,34 @@ sidebar_position: 6 When dealing with LLMs, errors are actually quite common, be it a connection failure on calling APIs or invalid parameters hallucinated by the model, so it's important to think carefully on how to handle exceptions. -To help with that, LiteChain provides an [`on_error`](pathname:///reference/litechain/index.html#litechain.Chain.on_error) method on chains which allows you to capture any unhandled exceptions mid-chain. +To help with that, LangStream provides an [`on_error`](pathname:///reference/langstream/index.html#langstream.Stream.on_error) method on streams which allows you to capture any unhandled exceptions mid-stream. -The `on_error` function takes a lambda with an exception as its argument and returns a new value that will be used as the output of the chain instead of the exception, you can also call another chain from within the `on_error` handler. +The `on_error` function takes a lambda with an exception as its argument and returns a new value that will be used as the output of the stream instead of the exception, you can also call another stream from within the `on_error` handler. Here is a simple example: ```python -from litechain import Chain +from langstream import Stream def failed_greeting(name: str): raise Exception(f"Giving {name} a cold shoulder") async def example(): - greet_chain = Chain[str, str]( - "GreetingChain", + greet_stream = Stream[str, str]( + "GreetingStream", failed_greeting ).on_error(lambda e: f"Sorry, an error occurred: {str(e)}") - async for output in greet_chain("Alice"): + async for output in greet_stream("Alice"): print(output) await example() -# ChainOutput(chain='GreetingChain', data=Exception('Giving Alice a cold shoulder'), final=False) -# ChainOutput(chain='GreetingChain@on_error', data='Sorry, an error occurred: Giving Alice a cold shoulder', final=True) +# StreamOutput(stream='GreetingStream', data=Exception('Giving Alice a cold shoulder'), final=False) +# StreamOutput(stream='GreetingStream@on_error', data='Sorry, an error occurred: Giving Alice a cold shoulder', final=True) ``` -You can then keep composing chains after the `on_error`, using methods like `map` or `and_then` after it. +You can then keep composing streams after the `on_error`, using methods like `map` or `and_then` after it. For a more complete example, check out the [Weather Bot with Error Handling](../examples/weather-bot-error-handling) example. -Now go to the next step to figure out how to build your own custom chains! \ No newline at end of file +Now go to the next step to figure out how to build your own custom streams! \ No newline at end of file diff --git a/docs/docs/stream-basics/index.md b/docs/docs/stream-basics/index.md new file mode 100644 index 0000000..c2be98a --- /dev/null +++ b/docs/docs/stream-basics/index.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 2 +--- + +# Stream Basics + +The [Stream](pathname:///reference/langstream/index.html#stream) is the main building block of LangStream, you compose streams together to build your LLM application. + +A Stream is basically a function that takes an input and produces an [`AsyncGenerator`](https://peps.python.org/pep-0525/) of an output, if you are not familiar with async generators, you can think about it as list over time. + +The simplest of all streams takes one input and produces a single item, and this is how you create one: + +```python +uppercase_stream = Stream[str, str]("UppercaseStream", lambda input: input.upper()) +``` + +As you can see, there are some parameters you pass to it, first of all is the type signature `[str, str]`, this defines the input and output types of the stream, respectively. In this case they are the same, but they could be different, you can read more about why types are important for LangStream [here](/docs/stream-basics/type_signatures). + +It also takes a name, `"UppercaseStream"`, the reason for having a name is making it easier to debug, so it can be anything you want, as long as it's helpful for you to identify later. If any issues arrive along the way, you can debug and visualize exactly which streams are misbehaving. + +Then, the heart of the stream, is the lambda function that is executed when the stream is called. It takes exactly one input (which is `str` in this) and must return a value of the specified output type (also `str`), here it just returns the same input but in uppercase. + +Now that we have a stream, we can just run it, as a function, and we will get back an [`AsyncGenerator`](https://peps.python.org/pep-0525/) of outputs that we can iterate on. Here is the full example: + +```python +from langstream import Stream +import asyncio + +async def example(): + uppercase_stream = Stream[str, str]("UppercaseStream", lambda input: input.upper()) + + async for output in uppercase_stream("i am not screaming"): + print(output.data) + +asyncio.run(example()) +#=> I AM NOT SCREAMING +``` + +As you can see, upon calling the stream, we had to iterate over it using `async for`, this loop will only run once, because our stream is producing a single value, but still, it is necessary since streams are always producing async generators. +Go to the next section to understand better why is that. \ No newline at end of file diff --git a/docs/docs/stream-basics/type_signatures.md b/docs/docs/stream-basics/type_signatures.md new file mode 100644 index 0000000..ba851aa --- /dev/null +++ b/docs/docs/stream-basics/type_signatures.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 5 +--- + +# Type Signatures + +In the recent years, Python has been expanding the support for [type hints](https://docs.python.org/3/library/typing.html), which helps a lot during development to catch bugs from types that should not be there, and even detecting `None`s before they happen. + +On quick scripts, notebooks, some web apps and throwaway code types might not be very useful and actually get in the way, however, when you are doing a lot of data piping and connecting many different pieces together, types are extremely useful, to help you save time and patience in debugging why two things are not working well together, and this is exactly what LangStream is about. + +Because of that, LangStream has a very thorough type annotation, with the goal of making it very reliable for the developer, but it works best when you explicitly declare the input and output types you expect when defining a stream: + +```python +len_stream = Stream[str, int]("LenStream", lambda input: len(input)) +``` + +This stream above for example, just counts the length of a string, so it takes a `str` and return an `int`. The nice side-effect of this is that you can see, at a glance, what goes in and what goes out of the stream, without needing to read it's implementation + +Now, Let's say you have this other stream: + +```python +happy_stream = Stream[str, str]("HappyStream", lambda input: input + " :)") +``` + +If you try to fit the two, it won't work, but instead of knowing that only when you run the code, you can find out instantly, if you use a code editor like VS Code (with PyLance extension for python type checking), it will look like this: + +![vscode showing typecheck error](/img/type-error-1.png) + +It says that `"Iterable[int]" is incompatible with "str"`, right, the first stream produces `int`s, so let's transform them to string by adding a `.map(str)`: + +![vscode showing typecheck error](/img/type-error-2.png) + +It still doesn't typecheck, but now it says that `"Iterable[str]" is incompatible with "str"`, of course, because streams produce a stream of things, not just one thing, and the `happy_stream` expect a single string, so this type error reminded us that we need to use `join()` first: + +![vscode showing no type errors](/img/type-error-3.png) + +Solved, no type errors now, with instant feedback. + +You don't necessarily need to add the types of your streams in LangStream, it can be inferenced from the lambda or function you pass, however, be aware it may end up being not what you want, for example: + +```python +first_item_stream = Stream("FirstItemStream", lambda input: input[0]) + +await collect_final_output(first_item_stream([1, 2, 3])) +#=> [1] + +await collect_final_output(first_item_stream("Foo Bar")) +#=> ["F"] +``` + +The first `first_item_stream` was intended to take the first item only from lists, but it also works with strings actually, since `[0]` works on strings. This might not be what you expected at first, and lead to annoying bugs. If, however, you are explicit about your types, then the second call will show a type error, helping you to notice this early on, maybe before you run it: + +```python +first_item_stream = Stream[List[int], int]("FirstItemStream", lambda input: input[0]) + +await collect_final_output(first_item_stream([1, 2, 3])) +#=> [1] + +# highlight-next-line +await collect_final_output(first_item_stream("Foo Bar")) # ❌ type error +#=> ["F"] +``` + +Now types cannot prevent all errors, some will still happen at runtime. Go to the next page to check out on error handling. \ No newline at end of file diff --git a/docs/docs/chain-basics/why_streams.md b/docs/docs/stream-basics/why_streams.md similarity index 78% rename from docs/docs/chain-basics/why_streams.md rename to docs/docs/stream-basics/why_streams.md index 2b4076a..d58a0d4 100644 --- a/docs/docs/chain-basics/why_streams.md +++ b/docs/docs/stream-basics/why_streams.md @@ -17,7 +17,7 @@ Granted, it is not always that we will need or even want the results to be strea Newton, and one as it if were Kanye West, and then generate the answer basing on the arguments of those three for a better result, then of course you need to wait for them to finish first before answering the user. It is, however, very easy to block a stream and wait for it to finish before moving on, but the other way around, taking a blocking operation, and making a stream out of it, is simply not possible. -That's why streaming is not just a feature for LiteChain, it is fundamentally ingrained into the [`Chain`](pathname:///reference/litechain/index.html#chain), under the hood everything is a Python [`AsyncGenerator`](https://peps.python.org/pep-0525/), -the Chain just give us a nice composable and debuggable interface on top. +That's why streaming is not just a feature for LangStream, it is fundamentally ingrained into the [`Stream`](pathname:///reference/langstream/index.html#stream), under the hood everything is a Python [`AsyncGenerator`](https://peps.python.org/pep-0525/), +the Stream just give us a nice composable and debuggable interface on top. Continue on reading for examples on how that looks like, and how do we work with those streams. \ No newline at end of file diff --git a/docs/docs/stream-basics/working_with_streams.md b/docs/docs/stream-basics/working_with_streams.md new file mode 100644 index 0000000..6f0ba7f --- /dev/null +++ b/docs/docs/stream-basics/working_with_streams.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 3 +--- + +# Working with Streams + +By default, all LLMs generate a stream of tokens: + +```python +from langstream.contrib import OpenAICompletionStream + +bacon_stream = OpenAICompletionStream[str, str]( + "BaconStream", + lambda input: input, + model="ada", +) + +async for output in bacon_stream("I like bacon and"): + print(output.data) +#=> iced +#=> tea +#=> . +#=> I +#=> like +#=> to +#=> eat +#=> bacon +#=> and +#=> +#=> iced +#=> tea +#=> . +``` + +You can notice that it will print more or less one word per line, those are the tokens it is generating, since Python by default adds a new line for each `print` statement, we end up with one token per line. + +When creating a simple Stream, if you return a single value, it will also output just that single value, so if you want to simulate an LLM, and create a stream that produces a stream of outputs, you can use the [`as_async_generator()`](pathname:///reference/langstream/index.html#langstream.as_async_generator) utility function: + + +```python +from langstream import Stream, as_async_generator + +stream_of_bacon_stream = Stream[None, str]( + "StreamOfBaconStream", + lambda _: as_async_generator("I", "like", "bacon"), +) + +async for output in stream_of_bacon_stream(None): + print(output.data) +#=> I +#=> like +#=> bacon +``` + +## All original outputs are streamed + +On LangStream, when you compose two or more streams, map the results or apply any operations on it, still the original values of anything generating outputs anywhere in the stream gets streamed, this means that if you have a stream being mapped, +both the original output and the transformed ones will be outputted, for example: + +```python +from langstream import Stream, as_async_generator + +stream_of_bacon_stream = Stream[None, str]( + "StreamOfBaconStream", + lambda _: as_async_generator("I", "like", "bacon"), +) + +tell_the_world = stream_of_bacon_stream.map(lambda token: token.upper()) + +async for output in tell_the_world(None): + print(output.stream, ":", output.data) +#=> StreamOfBaconStream : I +#=> StreamOfBaconStream@map : I +#=> StreamOfBaconStream : like +#=> StreamOfBaconStream@map : LIKE +#=> StreamOfBaconStream : bacon +#=> StreamOfBaconStream@map : BACON +``` + +This is done by design so that you can always inspect what is going in the middle of a complex stream, either to debug it, or to display to the user for a better user experience. + +If you want just the final output, you can check for the property [`output.final`](pathname:///reference/langstream/index.html#langstream.StreamOutput.final): + +```python +import time + +async for output in tell_the_world(None): + if output.final: + time.sleep(1) # added for dramatic effect + print(output.data) +#=> I +#=> LIKE +#=> BACON +``` + +## Output Utils + +Now, as shown on the examples, you need to iterate over it with `async for` to get the final output. However, you might not care about streaming or inspecting the middle results at all, and just want the final result as a whole. For that, you can use some utility functions that LangStream provides, for example, [`collect_final_output()`](pathname:///reference/langstream/index.html#langstream.collect_final_output), which gives you a single list with the final outputs all at once: + +```python +from langstream import collect_final_output + +await collect_final_output(tell_the_world(None)) +#=> ['I', 'LIKE', 'BACON'] +``` + +Or, if your stream's final output is `str`, then you can use [`join_final_output()`](pathname:///reference/langstream/index.html#langstream.join_final_output), which gives you already the full string, concatenated + +```python +from langstream import join_final_output + +await join_final_output(tell_the_world(None)) +#=> 'ILIKEBACON' +``` + +(LLMs produce spaces as token as well, so normally the lack of spaces in here is not a problem) + +Check out also [`filter_final_output()`](pathname:///reference/langstream/index.html#langstream.filter_final_output), which gives you still an `AsyncGenerator` to loop over, but including only the final results. + +Now that you know all about streams, you need to understand what does that mean when you are composing them together, keep on reading to learn about Composing Streams. \ No newline at end of file diff --git a/docs/docs/ui/chainlit.md b/docs/docs/ui/chainlit.md index 1dc40e7..d88a9f6 100644 --- a/docs/docs/ui/chainlit.md +++ b/docs/docs/ui/chainlit.md @@ -4,7 +4,7 @@ sidebar_position: 1 # Chainlit Integration -[Chainlit](https://github.com/Chainlit/chainlit) is a UI that gives you a ChatGPT like interface for your chains, it is very easy to set up, it has a slick UI, and it allows you to visualize the intermediary steps, so it's great for development! +[Chainlit](https://github.com/Chainlit/chainlit) is a UI that gives you a ChatGPT like interface for your streams, it is very easy to set up, it has a slick UI, and it allows you to visualize the intermediary steps, so it's great for development! You can install it with: @@ -12,7 +12,7 @@ You can install it with: pip install chainlit ``` -Then since we have access to all intermediary steps in LiteChain, integrating it with Chainlit is as easy as this: +Then since we have access to all intermediary steps in LangStream, integrating it with Chainlit is as easy as this: ```python from typing import Dict @@ -22,24 +22,24 @@ import chainlit as cl async def on_message(message: str): messages_map: Dict[str, cl.Message] = {} - async for output in chain(message): - if output.chain in messages_map: - cl_message = messages_map[output.chain] + async for output in stream(message): + if output.stream in messages_map: + cl_message = messages_map[output.stream] await cl_message.stream_token(output.data.content) else: - messages_map[output.chain] = cl.Message( - author=output.chain, + messages_map[output.stream] = cl.Message( + author=output.stream, content=output.data.content, indent=0 if output.final else 1, ) - await messages_map[output.chain].send() + await messages_map[output.stream].send() ``` -Here we are calling our chain, which is an [`OpenAIChatChain`](pathname:///reference/litechain/contrib/index.html#litechain.contrib.OpenAIChatChain), creating a new message as soon as a chain outputs, and streaming it new content as it arrives. We also `indent` the message to mark it as an intermediary step if the output is not `final`. +Here we are calling our stream, which is an [`OpenAIChatStream`](pathname:///reference/langstream/contrib/index.html#langstream.contrib.OpenAIChatStream), creating a new message as soon as a stream outputs, and streaming it new content as it arrives. We also `indent` the message to mark it as an intermediary step if the output is not `final`. Using our emoji translator example from before, this is how it is going to look like: - + Here is the complete code for an integration example: @@ -48,8 +48,8 @@ from typing import Dict, Iterable, List, Tuple, TypedDict import chainlit as cl -from litechain import debug -from litechain.contrib import OpenAIChatChain, OpenAIChatDelta, OpenAIChatMessage +from langstream import debug +from langstream.contrib import OpenAIChatStream, OpenAIChatDelta, OpenAIChatMessage class Memory(TypedDict): @@ -74,8 +74,8 @@ def update_delta_on_memory(delta: OpenAIChatDelta) -> OpenAIChatDelta: return delta -translator_chain = OpenAIChatChain[Iterable[OpenAIChatDelta], OpenAIChatDelta]( - "TranslatorChain", +translator_stream = OpenAIChatStream[Iterable[OpenAIChatDelta], OpenAIChatDelta]( + "TranslatorStream", lambda emoji_tokens: [ OpenAIChatMessage( role="user", @@ -85,10 +85,10 @@ translator_chain = OpenAIChatChain[Iterable[OpenAIChatDelta], OpenAIChatDelta]( model="gpt-4", ) -chain = ( +stream = ( debug( - OpenAIChatChain[str, OpenAIChatDelta]( - "EmojiChatChain", + OpenAIChatStream[str, OpenAIChatDelta]( + "EmojiChatStream", lambda user_message: [ *memory["history"], save_message_to_memory( @@ -102,27 +102,27 @@ chain = ( ) ) .map(update_delta_on_memory) - .and_then(debug(translator_chain)) + .and_then(debug(translator_stream)) ) @cl.on_message async def on_message(message: str): messages_map: Dict[str, Tuple[bool, cl.Message]] = {} - async for output in chain(message): - if "@" in output.chain and not output.final: + async for output in stream(message): + if "@" in output.stream and not output.final: continue - if output.chain in messages_map: - sent, cl_message = messages_map[output.chain] + if output.stream in messages_map: + sent, cl_message = messages_map[output.stream] if not sent: await cl_message.send() - messages_map[output.chain] = (True, cl_message) + messages_map[output.stream] = (True, cl_message) await cl_message.stream_token(output.data.content) else: - messages_map[output.chain] = ( + messages_map[output.stream] = ( False, cl.Message( - author=output.chain, + author=output.stream, content=output.data.content, indent=0 if output.final else 1, ), diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 9868468..5df71cd 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -6,8 +6,8 @@ const darkCodeTheme = require("prism-react-renderer/themes/dracula"); /** @type {import('@docusaurus/types').Config} */ const config = { - title: "LiteChain", - tagline: "Lightweight LLM chaining library", + title: "LangStream", + tagline: "Lightweight LLM streaming library", favicon: "data:image/svg+xml,🪽", @@ -15,14 +15,14 @@ const config = { url: "https://github.com/", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' - baseUrl: "/litechain", + baseUrl: "/langstream", staticDirectories: ["reference", "static"], // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: "rogeriochaves", // Usually your GitHub org/user name. - projectName: "litechain", // Usually your repo name. + projectName: "langstream", // Usually your repo name. onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", @@ -67,7 +67,7 @@ const config = { // Replace with your project's social card image: "img/docusaurus-social-card.jpg", navbar: { - title: "🪽🔗 LiteChain", + title: "🪽🔗 LangStream", items: [ { type: "docSidebar", @@ -77,10 +77,10 @@ const config = { }, { type: "html", - value: "Reference" + value: "Reference" }, { - href: "https://github.com/rogeriochaves/litechain", + href: "https://github.com/rogeriochaves/langstream", label: "GitHub", position: "right", }, @@ -108,7 +108,7 @@ const config = { ], }, ], - copyright: `Copyright © ${new Date().getFullYear()} LiteChain, Inc. Docs built with Docusaurus and pdoc3.`, + copyright: `Copyright © ${new Date().getFullYear()} LangStream, Inc. Docs built with Docusaurus and pdoc3.`, }, prism: { theme: lightCodeTheme, diff --git a/docs/package-lock.json b/docs/package-lock.json index 66b7097..b5a8aee 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -703,14 +703,14 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-streaming": { "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-streaming/-/plugin-bugfix-v8-spread-parameters-in-optional-streaming-7.22.5.tgz", "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" + "@babel/plugin-transform-optional-streaming": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -935,9 +935,9 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { + "node_modules/@babel/plugin-syntax-optional-streaming": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-streaming/-/plugin-syntax-optional-streaming-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1492,14 +1492,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-optional-chaining": { + "node_modules/@babel/plugin-transform-optional-streaming": { "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-streaming/-/plugin-transform-optional-streaming-7.22.5.tgz", "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/plugin-syntax-optional-streaming": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1856,7 +1856,7 @@ "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-streaming": "^7.22.5", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -1872,7 +1872,7 @@ "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-optional-streaming": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", @@ -1908,7 +1908,7 @@ "@babel/plugin-transform-object-rest-spread": "^7.22.5", "@babel/plugin-transform-object-super": "^7.22.5", "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-optional-streaming": "^7.22.5", "@babel/plugin-transform-parameters": "^7.22.5", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/plugin-transform-private-property-in-object": "^7.22.5", @@ -13249,14 +13249,14 @@ "@babel/helper-plugin-utils": "^7.22.5" } }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-streaming": { "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-streaming/-/plugin-bugfix-v8-spread-parameters-in-optional-streaming-7.22.5.tgz", "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", "requires": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" + "@babel/plugin-transform-optional-streaming": "^7.22.5" } }, "@babel/plugin-proposal-object-rest-spread": { @@ -13404,9 +13404,9 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-optional-chaining": { + "@babel/plugin-syntax-optional-streaming": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-streaming/-/plugin-syntax-optional-streaming-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "requires": { "@babel/helper-plugin-utils": "^7.8.0" @@ -13742,14 +13742,14 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" } }, - "@babel/plugin-transform-optional-chaining": { + "@babel/plugin-transform-optional-streaming": { "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-streaming/-/plugin-transform-optional-streaming-7.22.5.tgz", "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", "requires": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/plugin-syntax-optional-streaming": "^7.8.3" } }, "@babel/plugin-transform-parameters": { @@ -13967,7 +13967,7 @@ "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-streaming": "^7.22.5", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -13983,7 +13983,7 @@ "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-optional-streaming": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", @@ -14019,7 +14019,7 @@ "@babel/plugin-transform-object-rest-spread": "^7.22.5", "@babel/plugin-transform-object-super": "^7.22.5", "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-optional-streaming": "^7.22.5", "@babel/plugin-transform-parameters": "^7.22.5", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/plugin-transform-private-property-in-object": "^7.22.5", diff --git a/docs/pdoc_template/html.mako b/docs/pdoc_template/html.mako index b80ea09..56833af 100644 --- a/docs/pdoc_template/html.mako +++ b/docs/pdoc_template/html.mako @@ -299,9 +299,9 @@ <%include file="_lunr_search.inc.mako"/> % endif - ← Back to Docs + ← Back to Docs -

🪽🔗 LiteChain API Reference

+

🪽🔗 LangStream API Reference

${extract_toc(module.docstring) if extract_module_toc_into_sidebar else ''}
    % if supermodule: @@ -479,24 +479,24 @@ justify-content: space-between; width: 100%;">