From 7ac68a1614d62f5c48bdd689fa454089ea23a714 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:17 +0100 Subject: [PATCH 001/451] =?UTF-8?q?=F0=9F=94=84=20Update=20model=20provide?= =?UTF-8?q?rs=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MODELS.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/MODELS.md b/MODELS.md index f865603..9cdccf4 100644 --- a/MODELS.md +++ b/MODELS.md @@ -21,16 +21,15 @@ You can also set the `settings.llm` with a string identifier of a ChatModel incl ### Providers - `openai`: OpenAI Chat Models -- `gguf`: Huggingface GGUF Models from TheBloke using LlamaCpp -- `local` | `thebloke` | `huggingface`: alias for `gguf` +- `ollama`: Run local models through Ollama(llamacpp) ### Examples - `openai/gpt-3.5-turbo`: Classic ChatGPT -- `gguf/deepseek-llm-7b-chat`: DeepSeek LLM 7B Chat -- `gguf/OpenHermes-2.5-7B`: OpenHermes 2.5 -- `TheBloke/deepseek-llm-7B-chat-GGUF:Q3_K_M`: (eg thebloke huggingface identifier) -- `local/neural-chat-7B-v3-1`: Neural Chat 7B (local as alias for gguf) +- `openai/gpt-4-1106-preview`: GPT-4-Turbo +- `ollama/openchat-3.5- +- `ollama/openchat`: OpenChat3.5-1210 +- `ollama/openhermes2.5-mistral`: OpenHermes 2.5 ### additional notes From c666b72d50f9926c1d1625649e53a15658adc2b8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:17 +0100 Subject: [PATCH 002/451] =?UTF-8?q?=F0=9F=94=84=20Update=20model=20referen?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51f6824..666fdec 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ from pydantic import BaseModel, Field from funcchain import chain, settings # auto-download the model from huggingface -settings.llm = "gguf/openhermes-2.5-mistral-7b" +settings.llm = "ollama/openchat" class SentimentAnalysis(BaseModel): analysis: str From 20580a0efa0e822b294f6c62ec7fe6ef8d4dc755 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:17 +0100 Subject: [PATCH 003/451] =?UTF-8?q?=E2=9C=A8=20Add=20install=20method=20pr?= =?UTF-8?q?ompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev-setup.sh | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/dev-setup.sh b/dev-setup.sh index fee5319..ebd1b1d 100755 --- a/dev-setup.sh +++ b/dev-setup.sh @@ -3,16 +3,42 @@ # check if rye is installed if ! command -v rye &> /dev/null then - echo "rye could not be found: installing now ..." - curl -sSf https://rye-up.com/get | bash - echo "Check the rye docs for more info: https://rye-up.com/" + echo "rye could not be found" + echo "Would you like to install via rye or pip? Enter 'rye' or 'pip':" + read install_method + clear + + if [ "$install_method" = "rye" ] + then + echo "Installing via rye now ..." + curl -sSf https://rye-up.com/get | bash + echo "Check the rye docs for more info: https://rye-up.com/" + + elif [ "$install_method" = "pip" ] + then + echo "Installing via pip now ..." + python3 -m venv .venv + source .venv/bin/activate + pip install -r requirements.lock + + else + echo "Invalid option. Please run the script again and enter 'rye' or 'pip'." + exit 1 + fi + + clear fi -echo "SYNC: setup .venv" -rye sync +if [ "$install_method" = "rye" ] +then + echo "SYNC: setup .venv" + rye sync + + echo "ACTIVATE: activate .venv" + rye shell -echo "ACTIVATE: activate .venv" -rye shell + clear +fi echo "SETUP: install pre-commit hooks" pre-commit install From 11327c395710da8b5c4c9b6e6079ed6736d33c8f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:18 +0100 Subject: [PATCH 004/451] =?UTF-8?q?=F0=9F=94=84=20Update=20imports=20and?= =?UTF-8?q?=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/enums.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/enums.py b/examples/enums.py index 3efb97e..2304a27 100644 --- a/examples/enums.py +++ b/examples/enums.py @@ -1,7 +1,9 @@ -from funcchain import chain, settings -from pydantic import BaseModel from enum import Enum +from pydantic import BaseModel + +from funcchain import chain, settings + class Answer(str, Enum): yes = "yes" @@ -20,6 +22,6 @@ def make_decision(question: str) -> Decision: if __name__ == "__main__": - settings.llm = "gguf/phi-2" + settings.llm = "ollama/phi-2" print(make_decision("Do you like apples?")) From e99862519a5835bfa51ff5ca038a1fe56f3b65a9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:20 +0100 Subject: [PATCH 005/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20local=20codeblock?= =?UTF-8?q?=20generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/local_codeblock.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/experiments/local_codeblock.py diff --git a/examples/experiments/local_codeblock.py b/examples/experiments/local_codeblock.py new file mode 100644 index 0000000..0339c3f --- /dev/null +++ b/examples/experiments/local_codeblock.py @@ -0,0 +1,16 @@ +from funcchain import chain, settings +from funcchain.types import CodeBlock + + +def generate_code(instruction: str) -> CodeBlock: + return chain(instruction=instruction) + + +if __name__ == "__main__": + settings.llm = "ollama/openhermes-2.5-mistral-7b" + settings.console_stream = True + + block = generate_code("Write a script that generates a sin wave.") + + print("\033c") + print(block.code) From 7257ddab7ca7bb7401bb910cd168c247d083ceef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:20 +0100 Subject: [PATCH 006/451] =?UTF-8?q?=F0=9F=94=80=20Rename=20parallel=20stre?= =?UTF-8?q?aming=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../experiments/parallel_console_streaming.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 examples/experiments/parallel_console_streaming.py diff --git a/examples/experiments/parallel_console_streaming.py b/examples/experiments/parallel_console_streaming.py new file mode 100644 index 0000000..10dc354 --- /dev/null +++ b/examples/experiments/parallel_console_streaming.py @@ -0,0 +1,122 @@ +import asyncio +from contextlib import asynccontextmanager +from typing import AsyncGenerator +from uuid import uuid4 + +from rich.console import Console +from rich.layout import Layout +from rich.live import Live +from rich.panel import Panel + +from funcchain import achain, settings +from funcchain.streaming import astream_to +from funcchain.utils import count_tokens + + +class RenderChain: + def __init__(self, renderer: "Renderer", name: str) -> None: + self.id = uuid4().hex + self.name = name + self.renderer = renderer + self.renderer.add_chain(self) + + def render_stream(self, token: str) -> None: + self.renderer.render_stream(token, self) + + def close(self) -> None: + self.renderer.remove(self) + + +class Renderer: + def __init__(self, column_height: int = 3) -> None: + self.column_height = column_height + self.console = Console(height=3) + self.layout = Layout() + self.live = Live(console=self.console, auto_refresh=True, refresh_per_second=30) + self.chains: list[RenderChain] = [] + + def add_chain(self, chain: RenderChain) -> None: + if not self.live.is_started: + self.live.start() + self.console.height = (len(self.layout.children) + 1) * self.column_height + self.layout.split_column( + *self.layout.children, Layout(name=chain.id, size=self.column_height) + ) + self.chains.append(chain) + + def render_stream(self, token: str, chain: RenderChain) -> None: + prev = "" + tokens: int = 0 + max_width: int = self.console.width + content_width: int = 0 + if isinstance(panel := self.layout[chain.id]._renderable, Panel) and isinstance( + panel.renderable, str + ): + content_width = self.console.measure(panel.renderable).maximum + if isinstance(panel.title, str) and " " in panel.title: + tokens = int(panel.title.split(" ")[1]) + tokens += count_tokens(token) + prev = panel.renderable.replace("\n", " ") + if (max_width - content_width - 5) < 1: + prev = prev[len(token) :] + token + else: + prev += token + else: + prev += token + self.layout[chain.id].update( + Panel(prev, title=f"({chain.name}) {tokens} tokens") + ) + self.live.update(self.layout) + + def remove(self, chain: RenderChain) -> None: + self.chains.remove(chain) + self.layout.split_column( + *(child for child in self.layout.children if child.name != chain.id) + ) + self.console.height = (len(self.layout.children)) * self.column_height + self.live.update(self.layout) + if not self.chains: + self.live.update(self.layout) + self.live.stop() + + def __del__(self) -> None: + self.live.stop() + + +async def generate_poem_async(topic: str) -> str: + """ + Write a short story based on the topic. + """ + return await achain() + + +@asynccontextmanager +async def log_stream(renderer: Renderer, name: str) -> AsyncGenerator: + render_chain = RenderChain(renderer, name) + async with astream_to(render_chain.render_stream): + yield render_chain + + render_chain.close() + + +async def stream_poem_async(renderer: Renderer, topic: str) -> None: + async with log_stream(renderer, topic): + await generate_poem_async(topic) + + +async def main() -> None: + settings.llm = "openai/gpt-3.5-turbo-1106" + + topics = ["goldfish", "spacex", "samurai", "python", "javascript", "ai"] + renderer = Renderer() + + for topic in topics: + task = asyncio.create_task(stream_poem_async(renderer, topic)) + await asyncio.sleep(1) + + while not task.done(): + await asyncio.sleep(1) + + +asyncio.run(main()) +print("done") From bb244ee79cd6e5f1b8ce1ceedd1c177d23a6d325 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:21 +0100 Subject: [PATCH 007/451] =?UTF-8?q?=E2=9C=A8=20Add=20ranking=20functionali?= =?UTF-8?q?ty=20literals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/literals.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/literals.py diff --git a/examples/literals.py b/examples/literals.py new file mode 100644 index 0000000..54f9277 --- /dev/null +++ b/examples/literals.py @@ -0,0 +1,20 @@ +from typing import Literal +from funcchain import chain +from pydantic import BaseModel + + +class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + + +def rank_output(output: str) -> Ranking: + """ + Analyze and rank the output. + """ + return chain() + + +rank = rank_output("The quick brown fox jumps over the lazy dog.") + +print(rank) From 587ca6ec56c17a16388e8cf3491e6522ffbd8ab0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:21 +0100 Subject: [PATCH 008/451] =?UTF-8?q?=F0=9F=94=81=20Rename=20ollama.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/ollama.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/ollama.py diff --git a/examples/ollama.py b/examples/ollama.py new file mode 100644 index 0000000..0b76ca1 --- /dev/null +++ b/examples/ollama.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field + +from funcchain import chain, settings +from funcchain.streaming import stream_to + + +# define your model +class SentimentAnalysis(BaseModel): + analysis: str = Field(description="A description of the analysis") + sentiment: bool = Field(description="True for Happy, False for Sad") + + +# define your prompt +def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + + +if __name__ == "__main__": + # set global llm + settings.llm = "ollama/wizardcoder:34b-python-q3_K_M" + + # log tokens as stream to console + with stream_to(print): + # run prompt + poem = analyze("I really like when my dog does a trick!") + + # print final parsed output + from rich import print + + print(poem) From bc4332a06539b514e4ff932b4e5b616bf223f49e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:22 +0100 Subject: [PATCH 009/451] =?UTF-8?q?=F0=9F=93=84=20Add=20empty=20openai=5Fj?= =?UTF-8?q?son=5Fmode.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/openai_json_mode.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/openai_json_mode.py diff --git a/examples/openai_json_mode.py b/examples/openai_json_mode.py new file mode 100644 index 0000000..e69de29 From d22b5d24680b6ad87b99967ea3bc0bb7b780767e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:22 +0100 Subject: [PATCH 010/451] =?UTF-8?q?=F0=9F=94=84=20Update=20settings=20llm?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pydantic_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pydantic_validation.py b/examples/pydantic_validation.py index 7ddda1e..9fb3d33 100644 --- a/examples/pydantic_validation.py +++ b/examples/pydantic_validation.py @@ -3,7 +3,7 @@ from funcchain import chain, settings from funcchain.streaming import stream_to -settings.llm = "gguf/dolphin-2.5-mixtral-8x7b:Q3_K_M" +settings.llm = "ollama/dolphin-2.5-mixtral-8x7b:Q3_K_M" class Task(BaseModel): From 88469d6cbc75bfed46c4869ddb9e0072ddb733af Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:23 +0100 Subject: [PATCH 011/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20logging=20setti?= =?UTF-8?q?ngs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/router_chain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/router_chain.py b/examples/router_chain.py index d01a1a3..fe0f4a9 100644 --- a/examples/router_chain.py +++ b/examples/router_chain.py @@ -5,7 +5,8 @@ from funcchain import chain, settings -settings.llm = "gguf/openhermes-2.5-mistral-7b" +settings.console_stream = True +# settings.llm = "ollama/openhermes2.5-mistral" def handle_pdf_requests( From 54035d34c95943ddfc9fc2b50415dc1ddcf92875 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:23 +0100 Subject: [PATCH 012/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20router=20compon?= =?UTF-8?q?ent=20responses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/router_component.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/router_component.py b/examples/router_component.py index 1e3b618..ce81fb0 100644 --- a/examples/router_component.py +++ b/examples/router_component.py @@ -1,19 +1,22 @@ from funcchain.components import ChatRouter +from funcchain.settings import settings +settings.llm = "ollama/openchat" -def handle_pdf_requests(user_query: str) -> None: - print("Handling PDF requests with user query: ", user_query) +def handle_pdf_requests(user_query: str) -> str: + return "Handling PDF requests with user query: " + user_query -def handle_csv_requests(user_query: str) -> None: - print("Handling CSV requests with user query: ", user_query) +def handle_csv_requests(user_query: str) -> str: + return "Handling CSV requests with user query: " + user_query -def handle_default_requests(user_query: str) -> None: - print("Handling DEFAULT requests with user query: ", user_query) +def handle_default_requests(user_query: str) -> str: + return "Handling DEFAULT requests with user query: " + user_query -router = ChatRouter( + +router = ChatRouter[str]( routes={ "pdf": { "handler": handle_pdf_requests, From 057d28150c83c9c8288f6fa6da661d0e24e05b2c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:24 +0100 Subject: [PATCH 013/451] =?UTF-8?q?=F0=9F=94=BC=20Bump=20version=20to=20al?= =?UTF-8?q?pha.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d7b149..58c7d65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcchain" -version = "0.2.0-alpha.1" +version = "0.2.0-alpha.2" description = "🔖 write prompts as python functions" authors = [ { name = "Shroominic", email = "contact@shroominic.com" } From 93efe83e7a223e0722ca8be96325dd57670e213b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:24 +0100 Subject: [PATCH 014/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20=5Fllms.py=20?= =?UTF-8?q?classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/_llms.py | 391 ++++------------------------------------- 1 file changed, 32 insertions(+), 359 deletions(-) diff --git a/src/funcchain/_llms.py b/src/funcchain/_llms.py index e3b48ea..04c987e 100644 --- a/src/funcchain/_llms.py +++ b/src/funcchain/_llms.py @@ -1,368 +1,41 @@ -from __future__ import annotations +from typing import Any, Dict, Optional -import logging -from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, Union +from langchain_core.pydantic_v1 import validator +from langchain.chat_models import ChatOllama as _ChatOllama -from langchain_core.callbacks.manager import CallbackManagerForLLMRun -from langchain_core.language_models import BaseChatModel, BaseLanguageModel -from langchain_core.messages import ( - AIMessage, - AIMessageChunk, - BaseMessage, - ChatMessage, - HumanMessage, - SystemMessage, -) -from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult -from langchain_core.pydantic_v1 import Field, root_validator -from langchain_core.utils import get_pydantic_field_names -from langchain_core.utils.utils import build_extra_kwargs -logger = logging.getLogger(__name__) - - -class _LlamaCppCommon(BaseLanguageModel): - client: Any = Field(default=None, exclude=True) #: :meta private: - model_path: str - """The path to the Llama model file.""" - - lora_base: Optional[str] = None - """The path to the Llama LoRA base model.""" - - lora_path: Optional[str] = None - """The path to the Llama LoRA. If None, no LoRa is loaded.""" - - n_ctx: int = Field(4096, alias="n_ctx") - """Token context window.""" - - n_parts: int = Field(-1, alias="n_parts") - """Number of parts to split the model into. - If -1, the number of parts is automatically determined.""" - - seed: int = Field(-1, alias="seed") - """Seed. If -1, a random seed is used.""" - - f16_kv: bool = Field(True, alias="f16_kv") - """Use half-precision for key/value cache.""" - - logits_all: bool = Field(False, alias="logits_all") - """Return logits for all tokens, not just the last token.""" - - vocab_only: bool = Field(False, alias="vocab_only") - """Only load the vocabulary, no weights.""" - - use_mlock: bool = Field(False, alias="use_mlock") - """Force system to keep model in RAM.""" - - n_threads: Optional[int] = Field(None, alias="n_threads") - """Number of threads to use. - If None, the number of threads is automatically determined.""" - - n_batch: Optional[int] = Field(8, alias="n_batch") - """Number of tokens to process in parallel. - Should be a number between 1 and n_ctx.""" - - n_gpu_layers: Optional[int] = Field(None, alias="n_gpu_layers") - """Number of layers to be loaded into gpu memory. Default None.""" - - suffix: Optional[str] = Field(None) - """A suffix to append to the generated text. If None, no suffix is appended.""" - - max_tokens: Optional[int] = 512 - """The maximum number of tokens to generate.""" - - temperature: Optional[float] = 0.8 - """The temperature to use for sampling.""" - - top_p: Optional[float] = 0.95 - """The top-p value to use for sampling.""" - - logprobs: Optional[int] = Field(None) - """The number of logprobs to return. If None, no logprobs are returned.""" - - echo: Optional[bool] = False - """Whether to echo the prompt.""" - - stop: Optional[List[str]] = [] - """A list of strings to stop generation when encountered.""" - - repeat_penalty: Optional[float] = 1.1 - """The penalty to apply to repeated tokens.""" - - top_k: Optional[int] = 40 - """The top-k value to use for sampling.""" - - last_n_tokens_size: Optional[int] = 64 - """The number of tokens to look back when applying the repeat_penalty.""" - - use_mmap: Optional[bool] = True - """Whether to keep the model loaded in RAM""" - - rope_freq_scale: float = 1.0 - """Scale factor for rope sampling.""" - - rope_freq_base: float = 10000.0 - """Base frequency for rope sampling.""" - - model_kwargs: Dict[str, Any] = Field(default_factory=dict) - """Any additional parameters to pass to llama_cpp.Llama.""" - - streaming: bool = True - """Whether to stream the results, token by token.""" - - grammar_path: Optional[Union[str, Path]] = None - """ - grammar_path: Path to the .gbnf file that defines formal grammars - for constraining model outputs. For instance, the grammar can be used - to force the model to generate valid JSON or to speak exclusively in emojis. At most - one of grammar_path and grammar should be passed in. - """ +class ChatOllama(_ChatOllama): grammar: Optional[str] = None - """ - grammar: formal grammar for constraining model outputs. For instance, the grammar - can be used to force the model to generate valid JSON or to speak exclusively in - emojis. At most one of grammar_path and grammar should be passed in. - """ - - verbose: bool = False - """Print verbose output to stderr.""" - - @root_validator() - def validate_environment(cls, values: Dict) -> Dict: - """Validate that llama-cpp-python library is installed.""" - try: - from llama_cpp import Llama, LlamaGrammar - except ImportError: - raise ImportError( - "Could not import llama-cpp-python library. " - "Please install the llama-cpp-python library to " - "use this embedding model: pip install llama-cpp-python" - ) + """The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the model output. """ - model_path = values["model_path"] - model_param_names = [ - "rope_freq_scale", - "rope_freq_base", - "lora_path", - "lora_base", - "n_ctx", - "n_parts", - "seed", - "f16_kv", - "logits_all", - "vocab_only", - "use_mlock", - "n_threads", - "n_batch", - "use_mmap", - "last_n_tokens_size", - "verbose", - ] - model_params = {k: values[k] for k in model_param_names} - # For backwards compatibility, only include if non-null. - if values["n_gpu_layers"] is not None: - model_params["n_gpu_layers"] = values["n_gpu_layers"] - - model_params.update(values["model_kwargs"]) - - try: - values["client"] = Llama(model_path, **model_params) - except Exception as e: - raise ValueError( - f"Could not load Llama model from path: {model_path}. " - f"Received error {e}" - ) - - if values["grammar"] and values["grammar_path"]: - grammar = values["grammar"] - grammar_path = values["grammar_path"] - raise ValueError( - "Can only pass in one of grammar and grammar_path. Received " - f"{grammar=} and {grammar_path=}." - ) - elif isinstance(values["grammar"], str): - values["grammar"] = LlamaGrammar.from_string(values["grammar"]) - elif values["grammar_path"]: - values["grammar"] = LlamaGrammar.from_file(values["grammar_path"]) - else: - pass - return values - - @root_validator(pre=True) - def build_model_kwargs(cls, values: Dict[str, Any]) -> Dict[str, Any]: - """Build extra kwargs from additional params that were passed in.""" - all_required_field_names = get_pydantic_field_names(cls) - extra = values.get("model_kwargs", {}) - values["model_kwargs"] = build_extra_kwargs( - extra, values, all_required_field_names - ) - return values + @validator("grammar") + def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: + if v is not None and "root ::=" not in v: + raise ValueError("Grammar must contain a root rule.") + return v @property def _default_params(self) -> Dict[str, Any]: - """Get the default parameters for calling llama_cpp.""" - params = { - "suffix": self.suffix, - "max_tokens": self.max_tokens, - "temperature": self.temperature, - "top_p": self.top_p, - "logprobs": self.logprobs, - "echo": self.echo, - "stop_sequences": self.stop, # key here is convention among LLM classes - "repeat_penalty": self.repeat_penalty, - "top_k": self.top_k, + """Get the default parameters for calling Ollama.""" + return { + "model": self.model, + "format": self.format, + "options": { + "mirostat": self.mirostat, + "mirostat_eta": self.mirostat_eta, + "mirostat_tau": self.mirostat_tau, + "num_ctx": self.num_ctx, + "num_gpu": self.num_gpu, + "num_thread": self.num_thread, + "repeat_last_n": self.repeat_last_n, + "repeat_penalty": self.repeat_penalty, + "temperature": self.temperature, + "stop": self.stop, + "tfs_z": self.tfs_z, + "top_k": self.top_k, + "top_p": self.top_p, + "grammar": self.grammar, # added + }, + "system": self.system, + "template": self.template, } - if self.grammar: - params["grammar"] = self.grammar - return params - - @property - def _identifying_params(self) -> Dict[str, Any]: - """Get the identifying parameters.""" - return {**{"model_path": self.model_path}, **self._default_params} - - def _get_parameters(self, stop: Optional[List[str]] = None) -> Dict[str, Any]: - """ - Performs sanity check, preparing parameters in format needed by llama_cpp. - - Args: - stop (Optional[List[str]]): List of stop sequences for llama_cpp. - - Returns: - Dictionary containing the combined parameters. - """ - - # Raise error if stop sequences are in both input and default params - if self.stop and stop is not None: - raise ValueError("`stop` found in both the input and default params.") - - params = self._default_params - - # llama_cpp expects the "stop" key not this, so we remove it: - params.pop("stop_sequences") - - # then sets it as configured, or default to an empty list: - params["stop"] = self.stop or stop or [] - - return params - - def get_num_tokens(self, text: str) -> int: - tokenized_text = self.client.tokenize(text.encode("utf-8")) - return len(tokenized_text) - - -class ChatLlamaCpp(BaseChatModel, _LlamaCppCommon): - """llama.cpp chat model. - - To use, you should have the llama-cpp-python library installed, and provide the - path to the Llama model as a named parameter to the constructor. - Check out: https://github.com/abetlen/llama-cpp-python - - Example: - .. code-block:: python - - from funcchain._llms import ChatLlamaCpp - llm = ChatLlamaCpp(model_path="./path/to/model.gguf") - """ - - @property - def _llm_type(self) -> str: - """Return type of chat model.""" - return "llamacpp-chat" - - def _format_message_as_text(self, message: BaseMessage) -> str: - if isinstance(message, ChatMessage): - message_text = f"\n\n{message.role.capitalize()}: {message.content}" - elif isinstance(message, HumanMessage): - message_text = f"[INST] {message.content} [/INST]" - elif isinstance(message, AIMessage): - message_text = f"{message.content}" - elif isinstance(message, SystemMessage): - message_text = f"<> {message.content} <>" - else: - raise ValueError(f"Got unknown type {message}") - return message_text - - def _format_messages_as_text(self, messages: List[BaseMessage]) -> str: - return "\n".join( - [self._format_message_as_text(message) for message in messages] - ) - - def _stream_with_aggregation( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, - verbose: bool = False, - **kwargs: Any, - ) -> ChatGenerationChunk: - final_chunk: Optional[ChatGenerationChunk] = None - for chunk in self._stream(messages, stop, **kwargs): - if final_chunk is None: - final_chunk = chunk - else: - final_chunk += chunk - if run_manager: - run_manager.on_llm_new_token( - chunk.text, - verbose=verbose, - ) - if final_chunk is None: - raise ValueError("No data received from llamacpp stream.") - - return final_chunk - - def _generate( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, - **kwargs: Any, - ) -> ChatResult: - """Call out to LlamaCpp's generation endpoint. - - Args: - messages: The list of base messages to pass into the model. - stop: Optional list of stop words to use when generating. - - Returns: - Chat generations from the model - - Example: - .. code-block:: python - - response = llamacpp([ - HumanMessage(content="Tell me about the history of AI") - ]) - """ - final_chunk = self._stream_with_aggregation( - messages, stop=stop, run_manager=run_manager, verbose=self.verbose, **kwargs - ) - chat_generation = ChatGeneration( - message=AIMessage(content=final_chunk.text), - generation_info=final_chunk.generation_info, - ) - return ChatResult(generations=[chat_generation]) - - def _stream( - self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, - **kwargs: Any, - ) -> Iterator[ChatGenerationChunk]: - params = {**self._get_parameters(stop), **kwargs} - prompt = self._format_messages_as_text(messages) - result = self.client(prompt=prompt, stream=True, **params) - for part in result: - logprobs = part["choices"][0].get("logprobs", None) - chunk = ChatGenerationChunk( - message=AIMessageChunk(content=part["choices"][0]["text"]), - generation_info={"logprobs": logprobs}, - ) - yield chunk - if run_manager: - run_manager.on_llm_new_token( - token=chunk.text, verbose=self.verbose, log_probs=logprobs - ) From 60e84a5cc405f03327a83d2f53f8d0da85debcb3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:25 +0100 Subject: [PATCH 015/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/invoke.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/funcchain/chain/invoke.py b/src/funcchain/chain/invoke.py index f1b1454..5c783b8 100644 --- a/src/funcchain/chain/invoke.py +++ b/src/funcchain/chain/invoke.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Any +from typing import Any, TypeVar from langchain_core.callbacks.base import Callbacks from langchain_core.chat_history import BaseChatMessageHistory @@ -6,15 +6,15 @@ from langchain_core.runnables import RunnableSerializable from ..settings import FuncchainSettings -from .creation import create_chain from ..utils import ( from_docstring, get_output_type, - kwargs_from_parent, get_parent_frame, + kwargs_from_parent, log_openai_callback, retry_parse, ) +from .creation import create_chain T = TypeVar("T") From b91b5c3bc9077e798ac372f0230b88576b2617c4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:25 +0100 Subject: [PATCH 016/451] =?UTF-8?q?=E2=9C=A8=20Add=20format=20instructions?= =?UTF-8?q?=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/prompt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/funcchain/chain/prompt.py b/src/funcchain/chain/prompt.py index 5a887b7..80dab50 100644 --- a/src/funcchain/chain/prompt.py +++ b/src/funcchain/chain/prompt.py @@ -19,6 +19,7 @@ def create_instruction_prompt( instruction: str, images: list[Image.Image], input_kwargs: dict[str, Any], + format_instructions: Optional[str] = None, ) -> "HumanImageMessagePromptTemplate": template_format = _determine_format(instruction) @@ -40,6 +41,9 @@ def create_instruction_prompt( template=instruction, template_format=template_format, images=images, + partial_variables={"format_instructions": format_instructions} + if format_instructions + else None, ) From 410567035f13a1ac3649820d6b3bba0d5d5f883c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:26 +0100 Subject: [PATCH 017/451] =?UTF-8?q?=E2=9C=A8=20Add=20context=20and=20syste?= =?UTF-8?q?m=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/runnables.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/funcchain/chain/runnables.py b/src/funcchain/chain/runnables.py index 40689b4..55f52ad 100644 --- a/src/funcchain/chain/runnables.py +++ b/src/funcchain/chain/runnables.py @@ -11,6 +11,8 @@ def runnable( instruction: str, output_type: Type[T], input_args: list[str] = [], + context: list = [], + system: str = "", settings_override: SettingsOverride | None = None, ) -> RunnableSerializable[dict[str, str], T]: """ @@ -18,11 +20,11 @@ def runnable( """ instruction = "\n" + instruction chain: RunnableSerializable[dict[str, str], T] = create_chain( - "", - instruction, - output_type, - [], - ChatMessageHistory(), + system=system, + instruction=instruction, + output_type=output_type, + context=context, + memory=ChatMessageHistory(), settings=get_settings(settings_override), input_kwargs={k: "" for k in input_args}, ) From 64997dd7760b9c9fedc82653d4a87947e9dfb326 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:26 +0100 Subject: [PATCH 018/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20ChatRouter=20?= =?UTF-8?q?for=20extensibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/components.py | 189 +++++++++++++++++++++++++++++++++--- 1 file changed, 174 insertions(+), 15 deletions(-) diff --git a/src/funcchain/components.py b/src/funcchain/components.py index 4654dcf..b02562c 100644 --- a/src/funcchain/components.py +++ b/src/funcchain/components.py @@ -1,19 +1,29 @@ from enum import Enum -from typing import Union, Callable, TypedDict, Any, Coroutine -from pydantic import BaseModel, Field, field_validator +from typing import Iterator, Union, Callable, Any, TypeVar, Optional, AsyncIterator +from typing_extensions import TypedDict +from langchain_core.pydantic_v1 import validator as field_validator +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import HumanMessage +from langchain_core.runnables import Runnable, RunnableConfig, RunnableSerializable from funcchain import runnable +from .utils.msg_tools import msg_to_str class Route(TypedDict): - handler: Union[Callable, Coroutine] + handler: Callable description: str -Routes = dict[str, Union[Route, Callable, Coroutine]] +Routes = dict[str, Union[Route, Callable]] +ResponseType = TypeVar("ResponseType") -class ChatRouter(BaseModel): + +class ChatRouter(RunnableSerializable[HumanMessage, ResponseType]): routes: Routes + history: BaseChatMessageHistory | None = None + llm: BaseChatModel | str | None = None class Config: arbitrary_types_allowed = True @@ -24,12 +34,13 @@ def validate_routes(cls, v: Routes) -> Routes: raise ValueError("`default` route is missing") return v - def create_route(self) -> Any: + def create_route(self) -> Runnable[dict[str, str], Any]: RouteChoices = Enum( # type: ignore "RouteChoices", {r: r for r in self.routes.keys()}, type=str, ) + from pydantic import BaseModel, Field class RouterModel(BaseModel): selector: RouteChoices = Field( @@ -38,9 +49,11 @@ class RouterModel(BaseModel): ) return runnable( - instruction="Given the user query select the best query handler for it.", - input_args=["user_query", "query_handlers"], + instruction="Given the user request select the appropriate route.", + input_args=["user_request", "routes"], # todo: optional image output_type=RouterModel, + context=self.history.messages if self.history else [], + settings_override={"llm": self.llm}, ) def show_routes(self) -> str: @@ -53,14 +66,15 @@ def show_routes(self) -> str: ] ) - def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: + def invoke_route(self, user_query: str, /, **kwargs: Any) -> ResponseType: route_query = self.create_route() selected_route = route_query.invoke( input={ - "user_query": user_query, - "query_handlers": self.show_routes(), - } + "user_request": user_query, + "routes": self.show_routes(), + }, + config={"run_name": "ChatRouter"}, ).selector assert isinstance(selected_route, str) @@ -84,12 +98,157 @@ async def ainvoke_route(self, user_query: str, /, **kwargs: Any) -> Any: route_query = self.create_route() selected_route = route_query.invoke( input={ - "user_query": user_query, - "query_handlers": self.show_routes(), - } + "user_request": user_query, + "routes": self.show_routes(), + }, + config={"run_name": "ChatRouter"}, ).selector assert isinstance(selected_route, str) if isinstance(self.routes[selected_route], dict): return await self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore return await self.routes[selected_route](user_query, **kwargs) # type: ignore + + def invoke( + self, + input: HumanMessage, + config: Optional[RunnableConfig] = None, + **kwargs: Any, + ) -> ResponseType: + user_query = msg_to_str(input) + route_query = self.create_route() + + selected_route = route_query.invoke( + input={ + "user_request": user_query, + "routes": self.show_routes(), + }, + config=config.update({"run_name": "ChatRouter"}) if config else None, + ).selector + assert isinstance(selected_route, str) + + if isinstance(self.routes[selected_route], dict): + if hasattr(self.routes[selected_route]["handler"], "invoke"): # type: ignore + return self.routes[selected_route]["handler"].invoke( # type: ignore + input, config, **kwargs + ) + return self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore + + if hasattr(self.routes[selected_route], "invoke"): + return self.routes[selected_route].invoke(input, config, **kwargs) # type: ignore + return self.routes[selected_route](user_query, **kwargs) # type: ignore + + async def ainvoke( + self, + input: HumanMessage, + config: Optional[RunnableConfig] = None, + **kwargs: Any, + ) -> ResponseType: + user_query = msg_to_str(input) + import asyncio + + if not all( + [ + asyncio.iscoroutinefunction(route["handler"]) + if isinstance(route, dict) + else asyncio.iscoroutinefunction(route) + for route in self.routes.values() + ] + ): + raise ValueError("All routes must be awaitable when using `ainvoke_route`") + + route_query = self.create_route() + selected_route = route_query.invoke( + input={ + "user_request": user_query, + "routes": self.show_routes(), + }, + config=config.update({"run_name": "ChatRouter"}) if config else None, + ).selector + assert isinstance(selected_route, str) + + if isinstance(self.routes[selected_route], dict): + if hasattr(self.routes[selected_route]["handler"], "ainvoke"): # type: ignore + return await self.routes[selected_route]["handler"].ainvoke( # type: ignore + input, config, **kwargs + ) + return await self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore + + if hasattr(self.routes[selected_route], "ainvoke"): + return await self.routes[selected_route].ainvoke(input, config, **kwargs) # type: ignore + return await self.routes[selected_route](user_query, **kwargs) # type: ignore + + def stream( + self, + input: HumanMessage, + config: RunnableConfig | None = None, + **kwargs: Any | None, + ) -> Iterator[ResponseType]: + user_query = msg_to_str(input) + route_query = self.create_route() + + selected_route = route_query.invoke( + input={ + "user_request": user_query, + "routes": self.show_routes(), + }, + config=config.update({"run_name": "ChatRouter"}) if config else None, + ).selector + assert isinstance(selected_route, str) + + if isinstance(self.routes[selected_route], dict): + if hasattr(self.routes[selected_route]["handler"], "stream"): # type: ignore + yield from self.routes[selected_route]["handler"].stream( # type: ignore + input, config, **kwargs + ) + yield self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore + + if hasattr(self.routes[selected_route], "stream"): + yield from self.routes[selected_route].stream(input, config, **kwargs) # type: ignore + yield self.routes[selected_route](user_query, **kwargs) # type: ignore + + async def astream( + self, + input: HumanMessage, + config: RunnableConfig | None = None, + **kwargs: Any | None, + ) -> AsyncIterator[ResponseType]: + user_query = msg_to_str(input) + import asyncio + + if not all( + [ + asyncio.iscoroutinefunction(route["handler"]) + if isinstance(route, dict) + else asyncio.iscoroutinefunction(route) + for route in self.routes.values() + ] + ): + raise ValueError("All routes must be awaitable when using `ainvoke_route`") + + route_query = self.create_route() + selected_route = ( + await route_query.ainvoke( + input={ + "user_request": user_query, + "routes": self.show_routes(), + }, + config=config.update({"run_name": "ChatRouter"}) if config else None, + ) + ).selector + assert isinstance(selected_route, str) + + if isinstance(self.routes[selected_route], dict): + if hasattr(self.routes[selected_route]["handler"], "astream"): # type: ignore + async for item in self.routes[selected_route]["handler"].astream( # type: ignore + input, config, **kwargs + ): + yield item + yield await self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore + + if hasattr(self.routes[selected_route], "astream"): + async for item in self.routes[selected_route].astream( # type: ignore + input, config, **kwargs + ): + yield item + yield await self.routes[selected_route](user_query, **kwargs) # type: ignore From 9887adda4917ee349f022e71a411fcf55978a237 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:27 +0100 Subject: [PATCH 019/451] =?UTF-8?q?=F0=9F=94=84=20Switch=20JSON=20to=20YAM?= =?UTF-8?q?L?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/funcchain/parser.py b/src/funcchain/parser.py index 89bb445..6517565 100644 --- a/src/funcchain/parser.py +++ b/src/funcchain/parser.py @@ -1,5 +1,6 @@ import copy import json +import yaml # type: ignore import re from typing import Callable, Optional, Type, TypeVar @@ -229,12 +230,12 @@ def get_format_instructions(self) -> str: if "type" in reduced_schema: del reduced_schema["type"] # Ensure json in context is well-formed with double quotes. - schema_str = json.dumps(reduced_schema) + schema_str = yaml.dump(reduced_schema) return ( - "Please respond with a JSON object matching the following schema:" - f"\n\n```json_schema\n{schema_str}\n```" - "Only respond with the object, not the schema." + "Please respond with a json result matching the following schema:" + f"\n\n```schema\n{schema_str}\n```\n" + "Do not repeat the schema. Only respond with the result." ) @property From 1eb2044d64c83529d795b93f944e110c6f494ccc Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:28 +0100 Subject: [PATCH 020/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20settings=20st?= =?UTF-8?q?ructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/settings.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/funcchain/settings.py b/src/funcchain/settings.py index e1e9dd2..c79d3ab 100644 --- a/src/funcchain/settings.py +++ b/src/funcchain/settings.py @@ -2,27 +2,23 @@ Funcchain Settings: Automatically loads environment variables from .env file """ -from typing import Optional, TypedDict +from typing import Optional +from typing_extensions import TypedDict -from langchain.cache import InMemoryCache -from langchain_core.globals import set_llm_cache from langchain_core.language_models import BaseChatModel -from langchain_core.runnables import RunnableWithFallbacks from pydantic import Field from pydantic_settings import BaseSettings -set_llm_cache(InMemoryCache()) - class FuncchainSettings(BaseSettings): debug: bool = True - llm: BaseChatModel | RunnableWithFallbacks | str = Field( + llm: BaseChatModel | str = Field( default="openai/gpt-3.5-turbo-1106", validate_default=False, ) - local_models_path: str = "./.models" + console_stream: bool = False default_system_prompt: str = "" @@ -41,7 +37,7 @@ class FuncchainSettings(BaseSettings): max_tokens: int = 2048 temperature: float = 0.1 - # LLAMA KWARGS + # OLLAMA KWARGS context_lenght: int = 8196 n_gpu_layers: int = 50 keep_loaded: bool = False @@ -59,7 +55,7 @@ def openai_kwargs(self) -> dict: "openai_api_key": self.openai_api_key, } - def llama_kwargs(self) -> dict: + def ollama_kwargs(self) -> dict: return { "n_ctx": self.context_lenght, "use_mlock": self.keep_loaded, @@ -71,17 +67,19 @@ def llama_kwargs(self) -> dict: class SettingsOverride(TypedDict, total=False): - llm: BaseChatModel | RunnableWithFallbacks | str + llm: BaseChatModel | str | None verbose: bool temperature: float max_tokens: int streaming: bool - # TODO: context_length: int + context_lenght: int def get_settings(override: Optional[SettingsOverride] = None) -> FuncchainSettings: if override: + if override["llm"] is None: + override["llm"] = settings.llm return settings.model_copy(update=dict(override)) return settings From 865372fe5516366cabe47e969f3d8f20aaa211bd Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:28 +0100 Subject: [PATCH 021/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20types.py=20im?= =?UTF-8?q?ports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/types.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/funcchain/types.py b/src/funcchain/types.py index ac94ea3..c0e1ec9 100644 --- a/src/funcchain/types.py +++ b/src/funcchain/types.py @@ -2,7 +2,6 @@ import re from typing import Optional -from langchain.output_parsers.format_instructions import PYDANTIC_FORMAT_INSTRUCTIONS from langchain_core.exceptions import OutputParserException from langchain_core.output_parsers import BaseOutputParser from pydantic import BaseModel, Field @@ -26,11 +25,19 @@ def parse(cls, text: str) -> Self: if match: json_str = match.group() json_object = json.loads(json_str, strict=False) - return cls.parse_obj(json_object) + return cls.model_validate(json_object) @staticmethod def format_instructions() -> str: - return PYDANTIC_FORMAT_INSTRUCTIONS + return ( + "Please respond with a json result matching the following schema:" + "\n\n```schema\n{schema}\n```\n" + "Do not repeat the schema. Only respond with the result." + ) + + @staticmethod + def custom_grammar() -> str | None: + return None class CodeBlock(ParserBaseModel): @@ -63,6 +70,10 @@ def parse(cls, text: str) -> "CodeBlock": def format_instructions() -> str: return "Answer with a codeblock." + @staticmethod + def custom_grammar() -> str | None: + return 'root ::= "```" ([^`] | "`" [^`] | "``" [^`])* "```"' + def __str__(self) -> str: return self.code From 2498b320d3f5182d6be8ff11bc74dc66bbb861f3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:28 +0100 Subject: [PATCH 022/451] =?UTF-8?q?=F0=9F=94=A7=20Remove=20TODO=20placehol?= =?UTF-8?q?ders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/function_frame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/funcchain/utils/function_frame.py b/src/funcchain/utils/function_frame.py index b0f7418..3382a27 100644 --- a/src/funcchain/utils/function_frame.py +++ b/src/funcchain/utils/function_frame.py @@ -55,16 +55,16 @@ def get_output_type() -> type: raise ValueError("The funcchain must have a return type annotation") -def parser_for(output_type: type) -> BaseOutputParser: +def parser_for(output_type: type) -> BaseOutputParser | None: """ Get the parser from the type annotation of the parent caller function. """ if isinstance(output_type, types.UnionType): - return None # type: ignore # TODO: fix + return None # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) if getattr(output_type, "__origin__", None) is Union: output_type = output_type.__args__[0] # type: ignore - return None # type: ignore # TODO: fix + return None # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) if output_type is str: return StrOutputParser() From 3dda7077eb68a5021ed6c7c944d6e8a79d87de4d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:29 +0100 Subject: [PATCH 023/451] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20allOf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/grammars.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/funcchain/utils/grammars.py b/src/funcchain/utils/grammars.py index bd41804..e41e967 100644 --- a/src/funcchain/utils/grammars.py +++ b/src/funcchain/utils/grammars.py @@ -67,6 +67,15 @@ def visit(self, schema: dict, name: str) -> str: ) return self._add_rule(rule_name, rule) + elif "allOf" in schema: + rule = " ".join( + ( + self.visit(sub_schema, f'{name}{"-" if name else ""}{i}') + for i, sub_schema in enumerate(schema["allOf"]) + ) + ) + return self._add_rule(rule_name, rule) + elif "const" in schema: return self._add_rule(rule_name, self._format_literal(schema["const"])) From d03bdaa345ea05d75b6399c11a0f4506ce50cadd Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:29 +0100 Subject: [PATCH 024/451] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20Add=20base64=20?= =?UTF-8?q?to=20image=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/image.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/funcchain/utils/image.py b/src/funcchain/utils/image.py index 0bcb109..cfe282a 100644 --- a/src/funcchain/utils/image.py +++ b/src/funcchain/utils/image.py @@ -1,4 +1,4 @@ -from base64 import b64encode +from base64 import b64encode, b64decode from io import BytesIO from PIL import Image @@ -9,3 +9,10 @@ def image_to_base64_url(image: Image.Image) -> str: image.save(output, format="PNG") base64_image = b64encode(output.getvalue()).decode("utf-8") return f"data:image/jpeg;base64,{base64_image}" + + +def base64_url_to_image(base64_url: str) -> Image.Image: + base64_image = base64_url.split(",")[1] + image_bytes = b64decode(base64_image) + image = Image.open(BytesIO(image_bytes)) + return image From a622f80de7464d065b2d1feda292953c1fc84b4b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:30 +0100 Subject: [PATCH 025/451] =?UTF-8?q?=F0=9F=94=A5=20Refactor=20model=20selec?= =?UTF-8?q?tion=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/model_defaults.py | 137 +++++++------------------- 1 file changed, 37 insertions(+), 100 deletions(-) diff --git a/src/funcchain/utils/model_defaults.py b/src/funcchain/utils/model_defaults.py index 32bc80e..5791e31 100644 --- a/src/funcchain/utils/model_defaults.py +++ b/src/funcchain/utils/model_defaults.py @@ -1,90 +1,10 @@ -from pathlib import Path from typing import Any -from langchain.chat_models import ( - AzureChatOpenAI, - ChatAnthropic, - ChatGooglePalm, - ChatOpenAI, -) from langchain_core.language_models import BaseChatModel -from .._llms import ChatLlamaCpp from ..settings import FuncchainSettings -def get_gguf_model( - name: str, - label: str, - settings: FuncchainSettings, -) -> Path: - """ - Gather GGUF model from huggingface/TheBloke - - possible input: - - DiscoLM-mixtral-8x7b-v2-GGUF - - TheBloke/DiscoLM-mixtral-8x7b-v2 - - discolm-mixtral-8x7b-v2 - ... - - Raises ModelNotFound(name) error in case of no result. - """ - from huggingface_hub import hf_hub_download - - name = name.removesuffix("-GGUF") - label = "Q5_K_M" if label == "latest" else label - - model_path = Path(settings.local_models_path) - - if (p := model_path / f"{name.lower()}.{label}.gguf").exists(): - return p - - # check if available on huggingface - try: - # check local cache - - input( - f"Do you want to download this model from huggingface.co/TheBloke/{name}-GGUF ?\n" - "Press enter to continue." - ) - print("\033c") - print("Downloading model from huggingface...") - p = hf_hub_download( - repo_id=f"TheBloke/{name}-GGUF", - filename=f"{name.lower()}.{label}.gguf", - local_dir=model_path, - local_dir_use_symlinks=True, - ) - print("\033c") - return Path(p) - except Exception as e: - print(e) - raise ValueError(f"ModelNotFound: {name}.{label}") - - -def default_model_fallback( - settings: FuncchainSettings, - **model_kwargs: Any, -) -> ChatLlamaCpp: - """ - Give user multiple options for local models to download. - """ - if ( - input("ModelNotFound: Do you want to download a local model instead?") - .lower() - .startswith("y") - ): - model_kwargs.update(settings.llama_kwargs()) - return ChatLlamaCpp( - model_path=get_gguf_model( - "neural-chat-7b-v3-1", "Q4_K_M", settings - ).as_posix(), - **model_kwargs, - ) - print("Please select a model to use funcchain!") - exit(0) - - def univeral_model_selector( settings: FuncchainSettings, **model_kwargs: Any, @@ -101,12 +21,10 @@ def univeral_model_selector( Examples: - "openai/gpt-3.5-turbo" - "anthropic/claude-2" - - "thebloke/deepseek-llm-7b-chat" - - (gguf models from huggingface.co/TheBloke) + - "ollama/deepseek-llm-7b-chat" Supported: - [ openai, anthropic, google, llamacpp ] + [ openai, anthropic, google, ollama ] Raises: - ModelNotFoundError, when the model is not found. @@ -115,12 +33,7 @@ def univeral_model_selector( model_kwargs.update(settings.model_kwargs()) if model_name: - mtype, name_lable = ( - model_name.split("/") if "/" in model_name else ("", model_name) - ) - name, label = ( - name_lable.split(":") if ":" in name_lable else (name_lable, "latest") - ) + mtype, name = model_name.split("/") if "/" in model_name else ("", model_name) mtype = mtype.lower() model_kwargs["model_name"] = name @@ -128,42 +41,66 @@ def univeral_model_selector( try: match mtype: case "openai": + from langchain.chat_models import ChatOpenAI + model_kwargs.update(settings.openai_kwargs()) return ChatOpenAI(**model_kwargs) + case "anthropic": + from langchain.chat_models import ChatAnthropic + return ChatAnthropic(**model_kwargs) + case "google": + from langchain.chat_models import ChatGooglePalm + return ChatGooglePalm(**model_kwargs) - case "llamacpp" | "thebloke" | "huggingface" | "local" | "gguf": - model_kwargs.pop("model_name") - model_path = get_gguf_model(name, label, settings).as_posix() - print("Using model:", model_path) - model_kwargs.update(settings.llama_kwargs()) - return ChatLlamaCpp( - model_path=model_path, - **model_kwargs, - ) + + case "ollama": + from .._llms import ChatOllama + + model = model_kwargs.pop("model_name") + model_kwargs.update(settings.ollama_kwargs()) + return ChatOllama(model=model, **model_kwargs) + except Exception as e: print("ERROR:", e) raise e try: if "gpt-4" in name or "gpt-3.5" in name: + from langchain.chat_models import ChatOpenAI + model_kwargs.update(settings.openai_kwargs()) return ChatOpenAI(**model_kwargs) + except Exception as e: print(e) model_kwargs.pop("model_name") if settings.openai_api_key: + from langchain.chat_models import ChatOpenAI + model_kwargs.update(settings.openai_kwargs()) return ChatOpenAI(**model_kwargs) + if settings.azure_api_key: + from langchain.chat_models import AzureChatOpenAI + return AzureChatOpenAI(**model_kwargs) + if settings.anthropic_api_key: + from langchain.chat_models import ChatAnthropic + return ChatAnthropic(**model_kwargs) + if settings.google_api_key: + from langchain.chat_models import ChatGooglePalm + return ChatGooglePalm(**model_kwargs) - return default_model_fallback(**model_kwargs) + raise ValueError( + "Could not read llm selector string. " + "Please check [here](https://github.com/shroominic/funcchain/blob/main/MODELS.md) for more info." + ) From 608b199f0b1e6d61a23a30c3375f3e70b68ba4f8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:30 +0100 Subject: [PATCH 026/451] =?UTF-8?q?=E2=9C=A8=20Add=20message=20utility=20f?= =?UTF-8?q?unctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/msg_tools.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/funcchain/utils/msg_tools.py diff --git a/src/funcchain/utils/msg_tools.py b/src/funcchain/utils/msg_tools.py new file mode 100644 index 0000000..a4799c6 --- /dev/null +++ b/src/funcchain/utils/msg_tools.py @@ -0,0 +1,27 @@ +from typing import Union +from langchain_core.messages import BaseMessageChunk, BaseMessage as _BaseMessage + + +BaseMessage = Union[_BaseMessage, BaseMessageChunk] + + +def msg_images(msg: BaseMessage) -> list[str]: + """Return a list of image URLs in the message content.""" + if isinstance(msg.content, str): + return [] + return [ + item["image_url"]["url"] + for item in msg.content + if isinstance(item, dict) and item["type"] == "image_url" + ] + + +def msg_to_str(msg: BaseMessage) -> str: + """Return the message content.""" + return ( + msg.content + if isinstance(msg.content, str) + else msg.content[0] + if isinstance(msg.content[0], str) + else msg.content[0]["text"] + ) From 4e9d82a6ad2a7f861d9de643a6818b44a980715e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:31 +0100 Subject: [PATCH 027/451] =?UTF-8?q?=F0=9F=93=9D=20Renamed=20ollama=5Ftest.?= =?UTF-8?q?py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ollama_test.py | 84 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/ollama_test.py diff --git a/tests/ollama_test.py b/tests/ollama_test.py new file mode 100644 index 0000000..6c6f7f3 --- /dev/null +++ b/tests/ollama_test.py @@ -0,0 +1,84 @@ +import pytest +from pydantic import BaseModel + +from funcchain import chain, settings + + +class Task(BaseModel): + description: str + difficulty: int + + +class TodoList(BaseModel): + tasks: list[Task] + + +def todo_list(job_title: str) -> TodoList: + """ + Create a todo list for a perfect day for the given job. + """ + return chain() + + +@pytest.mark.skip_on_actions +def test_openhermes() -> None: + settings.llm = "ollama/openhermes2.5-mistral" + + assert isinstance( + todo_list("software engineer"), + TodoList, + ) + + +@pytest.mark.skip_on_actions +def test_neural_chat() -> None: + settings.llm = "ollama/openchat" + + assert isinstance( + todo_list("ai engineer"), + TodoList, + ) + + +# def test_vision() -> None: +# from PIL import Image + +# settings.llm = "mys/ggml_llava-v1.5-13b" + +# class Analysis(BaseModel): +# description: str = Field(description="A description of the image") +# objects: list[str] = Field(description="A list of objects found in the image") + +# def analyse(image: Image.Image) -> Analysis: +# """ +# Analyse the image and extract its +# theme, description and objects. +# """ +# return chain() + +# assert isinstance( +# analyse(Image.open("examples/assets/old_chinese_temple.jpg")), +# Analysis, +# ) + +# TODO: Test union types +# def test_union_types() -> None: +# ... + + +def test_model_search_failure() -> None: + settings.llm = "ollama/neural-chat-ultra-mega" + + try: + todo_list("software engineer") + except Exception: + assert True + else: + assert False, "Model should not be found" + + +if __name__ == "__main__": + test_openhermes() + test_neural_chat() + # test_vision() + test_model_search_failure() From f882f24c62cdfb4cdb33a7d3c40ef9a6c20a25ce Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:31 +0100 Subject: [PATCH 028/451] =?UTF-8?q?=E2=9C=A8=20Enhance=20router=20typing,?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/router_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/router_test.py b/tests/router_test.py index 67b38a6..4b4b5b4 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -13,7 +13,7 @@ def handle_default_requests(user_query: str) -> str: return f"Handling DEFAULT requests with user query: {user_query}" -router = ChatRouter( +router = ChatRouter[str]( routes={ "pdf": { "handler": handle_pdf_requests, @@ -33,7 +33,11 @@ def test_router() -> None: assert "Handling PDF" in router.invoke_route("Can you summarize this pdf?") - assert "Handling DEFAULT" in router.invoke_route("Hey, whatsup?") + assert "Handling DEFAULT" in router.invoke_route("whatsup") + + assert "Handling DEFAULT" in router.invoke_route( + "I want to book a flight how to do this?" + ) if __name__ == "__main__": From e5e099af143dc2d3d23cfb51e68e0e88249cccf7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:40 +0100 Subject: [PATCH 029/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20testing=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/console_log_testing.py | 122 -------------------- 1 file changed, 122 deletions(-) delete mode 100644 examples/experiments/console_log_testing.py diff --git a/examples/experiments/console_log_testing.py b/examples/experiments/console_log_testing.py deleted file mode 100644 index 10dc354..0000000 --- a/examples/experiments/console_log_testing.py +++ /dev/null @@ -1,122 +0,0 @@ -import asyncio -from contextlib import asynccontextmanager -from typing import AsyncGenerator -from uuid import uuid4 - -from rich.console import Console -from rich.layout import Layout -from rich.live import Live -from rich.panel import Panel - -from funcchain import achain, settings -from funcchain.streaming import astream_to -from funcchain.utils import count_tokens - - -class RenderChain: - def __init__(self, renderer: "Renderer", name: str) -> None: - self.id = uuid4().hex - self.name = name - self.renderer = renderer - self.renderer.add_chain(self) - - def render_stream(self, token: str) -> None: - self.renderer.render_stream(token, self) - - def close(self) -> None: - self.renderer.remove(self) - - -class Renderer: - def __init__(self, column_height: int = 3) -> None: - self.column_height = column_height - self.console = Console(height=3) - self.layout = Layout() - self.live = Live(console=self.console, auto_refresh=True, refresh_per_second=30) - self.chains: list[RenderChain] = [] - - def add_chain(self, chain: RenderChain) -> None: - if not self.live.is_started: - self.live.start() - self.console.height = (len(self.layout.children) + 1) * self.column_height - self.layout.split_column( - *self.layout.children, Layout(name=chain.id, size=self.column_height) - ) - self.chains.append(chain) - - def render_stream(self, token: str, chain: RenderChain) -> None: - prev = "" - tokens: int = 0 - max_width: int = self.console.width - content_width: int = 0 - if isinstance(panel := self.layout[chain.id]._renderable, Panel) and isinstance( - panel.renderable, str - ): - content_width = self.console.measure(panel.renderable).maximum - if isinstance(panel.title, str) and " " in panel.title: - tokens = int(panel.title.split(" ")[1]) - tokens += count_tokens(token) - prev = panel.renderable.replace("\n", " ") - if (max_width - content_width - 5) < 1: - prev = prev[len(token) :] + token - else: - prev += token - else: - prev += token - self.layout[chain.id].update( - Panel(prev, title=f"({chain.name}) {tokens} tokens") - ) - self.live.update(self.layout) - - def remove(self, chain: RenderChain) -> None: - self.chains.remove(chain) - self.layout.split_column( - *(child for child in self.layout.children if child.name != chain.id) - ) - self.console.height = (len(self.layout.children)) * self.column_height - self.live.update(self.layout) - if not self.chains: - self.live.update(self.layout) - self.live.stop() - - def __del__(self) -> None: - self.live.stop() - - -async def generate_poem_async(topic: str) -> str: - """ - Write a short story based on the topic. - """ - return await achain() - - -@asynccontextmanager -async def log_stream(renderer: Renderer, name: str) -> AsyncGenerator: - render_chain = RenderChain(renderer, name) - async with astream_to(render_chain.render_stream): - yield render_chain - - render_chain.close() - - -async def stream_poem_async(renderer: Renderer, topic: str) -> None: - async with log_stream(renderer, topic): - await generate_poem_async(topic) - - -async def main() -> None: - settings.llm = "openai/gpt-3.5-turbo-1106" - - topics = ["goldfish", "spacex", "samurai", "python", "javascript", "ai"] - renderer = Renderer() - - for topic in topics: - task = asyncio.create_task(stream_poem_async(renderer, topic)) - await asyncio.sleep(1) - - while not task.done(): - await asyncio.sleep(1) - - -asyncio.run(main()) -print("done") From ee68c42acec2733761242414cdd36080f3b85a68 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:41 +0100 Subject: [PATCH 030/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20llamac?= =?UTF-8?q?pp.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/llamacpp.py | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 examples/llamacpp.py diff --git a/examples/llamacpp.py b/examples/llamacpp.py deleted file mode 100644 index 77d382e..0000000 --- a/examples/llamacpp.py +++ /dev/null @@ -1,33 +0,0 @@ -from pydantic import BaseModel, Field - -from funcchain import chain, settings -from funcchain.streaming import stream_to - - -# define your model -class SentimentAnalysis(BaseModel): - analysis: str = Field(description="A description of the analysis") - sentiment: bool = Field(description="True for Happy, False for Sad") - - -# define your prompt -def analyze(text: str) -> SentimentAnalysis: - """ - Determines the sentiment of the text. - """ - return chain() - - -if __name__ == "__main__": - # set global llm - settings.llm = "gguf/openhermes-2.5-mistral-7b" - - # log tokens as stream to console - with stream_to(print): - # run prompt - poem = analyze("I really like when my dog does a trick!") - - # print final parsed output - from rich import print - - print(poem) From a9b0fe1d507692a0ee8b09ba05249241a379a8e4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:48:41 +0100 Subject: [PATCH 031/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20llamac?= =?UTF-8?q?pp=5Ftest.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/llamacpp_test.py | 84 ------------------------------------------ 1 file changed, 84 deletions(-) delete mode 100644 tests/llamacpp_test.py diff --git a/tests/llamacpp_test.py b/tests/llamacpp_test.py deleted file mode 100644 index adf8c01..0000000 --- a/tests/llamacpp_test.py +++ /dev/null @@ -1,84 +0,0 @@ -import pytest -from pydantic import BaseModel - -from funcchain import chain, settings - - -class Task(BaseModel): - description: str - difficulty: int - - -class TodoList(BaseModel): - tasks: list[Task] - - -def todo_list(job_title: str) -> TodoList: - """ - Create a todo list for a perfect day for the given job. - """ - return chain() - - -@pytest.mark.skip_on_actions -def test_openhermes() -> None: - settings.llm = "gguf/openhermes-2.5-mistral-7b" - - assert isinstance( - todo_list("software engineer"), - TodoList, - ) - - -@pytest.mark.skip_on_actions -def test_neural_chat() -> None: - settings.llm = "gguf/neural-chat-7b-v3-1" - - assert isinstance( - todo_list("ai engineer"), - TodoList, - ) - - -# def test_vision() -> None: -# from PIL import Image - -# settings.llm = "mys/ggml_llava-v1.5-13b" - -# class Analysis(BaseModel): -# description: str = Field(description="A description of the image") -# objects: list[str] = Field(description="A list of objects found in the image") - -# def analyse(image: Image.Image) -> Analysis: -# """ -# Analyse the image and extract its -# theme, description and objects. -# """ -# return chain() - -# assert isinstance( -# analyse(Image.open("examples/assets/old_chinese_temple.jpg")), -# Analysis, -# ) - -# TODO: Test union types -# def test_union_types() -> None: -# ... - - -def test_model_search_failure() -> None: - settings.llm = "gguf/neural-chat-ultra-mega" - - try: - todo_list("software engineer") - except Exception: - assert True - else: - assert False, "Model should not be found" - - -if __name__ == "__main__": - test_openhermes() - test_neural_chat() - # test_vision() - test_model_search_failure() From 224f143caa8d12181c8ccbb0c2be141867f5cfb4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 26 Dec 2023 20:49:22 +0100 Subject: [PATCH 032/451] =?UTF-8?q?=F0=9F=94=A7=20improve=20chain=20creati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/creation.py | 161 +++++++++++++++++--------------- 1 file changed, 86 insertions(+), 75 deletions(-) diff --git a/src/funcchain/chain/creation.py b/src/funcchain/chain/creation.py index afdefb6..31dcc0a 100644 --- a/src/funcchain/chain/creation.py +++ b/src/funcchain/chain/creation.py @@ -1,29 +1,25 @@ from types import UnionType -from typing import TypeVar, Type +from typing import Type, TypeVar +from langchain_core.callbacks import Callbacks +from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.language_models import BaseChatModel -from langchain_core.prompts import ChatPromptTemplate from langchain_core.messages import AIMessage, BaseMessage, HumanMessage from langchain_core.output_parsers import BaseOutputParser -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.runnables import ( - RunnableSerializable, - RunnableWithFallbacks, -) +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnableSerializable from PIL import Image from pydantic import BaseModel -from funcchain._llms import ChatLlamaCpp - from ..parser import MultiToolParser, ParserBaseModel, PydanticFuncParser from ..settings import FuncchainSettings from ..streaming import stream_handler from ..utils import ( - parser_for, count_tokens, is_function_model, is_vision_model, multi_pydantic_to_functions, + parser_for, pydantic_to_functions, pydantic_to_grammar, univeral_model_selector, @@ -44,7 +40,7 @@ def create_union_chain( system: str, memory: BaseChatMessageHistory, context: list[BaseMessage], - llm: BaseChatModel | RunnableWithFallbacks, + llm: BaseChatModel, input_kwargs: dict[str, str], ) -> RunnableSerializable[dict[str, str], BaseModel]: """ @@ -64,16 +60,7 @@ def create_union_chain( functions = multi_pydantic_to_functions(output_types) - if isinstance(llm, RunnableWithFallbacks): - llm = llm.runnable.bind(**functions).with_fallbacks( - [ - fallback.bind(**functions) - for fallback in llm.fallbacks - if hasattr(llm, "fallbacks") - ] - ) - else: - llm = llm.bind(**functions) # type: ignore + llm = llm.bind(**functions) # type: ignore prompt = create_chat_prompt( system, @@ -93,24 +80,15 @@ def create_union_chain( def create_pydanctic_chain( output_type: type[BaseModel], prompt: ChatPromptTemplate, - llm: BaseChatModel | RunnableWithFallbacks, + llm: BaseChatModel, input_kwargs: dict[str, str], ) -> RunnableSerializable[dict[str, str], BaseModel]: # TODO: check these format_instructions input_kwargs["format_instructions"] = f"Extract to {output_type.__name__}." functions = pydantic_to_functions(output_type) - llm = ( - llm.runnable.bind(**functions).with_fallbacks( # type: ignore - [ - fallback.bind(**functions) - for fallback in llm.fallbacks - if hasattr(llm, "fallbacks") - ] - ) - if isinstance(llm, RunnableWithFallbacks) - else llm.bind(**functions) - ) + llm = llm.bind(**functions) # type: ignore + return prompt | llm | PydanticFuncParser(pydantic_schema=output_type) @@ -127,13 +105,17 @@ def create_chain( Compile a langchain runnable chain from the funcchain syntax. """ # large language model - llm = _gather_llm(settings) + _llm = _gather_llm(settings) + llm = _add_custom_callbacks(_llm, settings) parser = parser_for(output_type) # add format instructions for parser - if parser and not is_function_model(llm): - instruction = _add_format_instructions( + f_instructions = None + if parser and (settings.streaming or not is_function_model(llm)): + # streaming behavior is not supported for function models + # but for normal function models we do not need to add format instructions + instruction, f_instructions = _add_format_instructions( parser, instruction, input_kwargs, @@ -151,29 +133,18 @@ def create_chain( images = _handle_images(llm, input_kwargs) # create prompts - instruction_prompt = create_instruction_prompt(instruction, images, input_kwargs) + instruction_prompt = create_instruction_prompt( + instruction, + images, + input_kwargs, + format_instructions=f_instructions, + ) chat_prompt = create_chat_prompt(system, instruction_prompt, context, memory) # add formatted instruction to chat history memory.add_message(instruction_prompt.format(**input_kwargs)) - if isinstance(llm, ChatLlamaCpp): - if isinstance(output_type, UnionType): - # TODO: implement Union Type grammar - raise NotImplementedError( - "Union types are not yet supported for LlamaCpp models." - ) - if issubclass(output_type, BaseModel) and not issubclass( - output_type, ParserBaseModel - ): - from llama_cpp import LlamaGrammar - - grammar = pydantic_to_grammar(output_type) - setattr( - llm, - "grammar", - LlamaGrammar.from_string(grammar, verbose=False), - ) + _inject_grammar_for_local_models(llm, output_type) # function model patches if is_function_model(llm): @@ -191,13 +162,16 @@ def create_chain( if issubclass(output_type, BaseModel) and not issubclass( output_type, ParserBaseModel ): - return create_pydanctic_chain( # type: ignore - output_type, - chat_prompt, - llm, - input_kwargs, - ) - + if settings.streaming and hasattr(llm, "model_kwargs"): + llm.model_kwargs = {"response_format": {"type": "json_object"}} + else: + return create_pydanctic_chain( # type: ignore + output_type, + chat_prompt, + llm, + input_kwargs, + ) + assert parser is not None return chat_prompt | llm | parser @@ -205,7 +179,7 @@ def _add_format_instructions( parser: BaseOutputParser, instruction: str, input_kwargs: dict[str, str], -) -> str: +) -> tuple[str, str | None]: """ Add parsing format instructions to the instruction message and input_kwargs @@ -215,9 +189,9 @@ def _add_format_instructions( if format_instructions := parser.get_format_instructions(): instruction += "\n{format_instructions}" input_kwargs["format_instructions"] = format_instructions - return instruction + return instruction, format_instructions except NotImplementedError: - return instruction + return instruction, None def _crop_large_inputs( @@ -239,7 +213,7 @@ def _crop_large_inputs( def _handle_images( - llm: BaseChatModel | RunnableWithFallbacks, + llm: BaseChatModel, input_kwargs: dict[str, str], ) -> list[Image.Image]: """ @@ -256,12 +230,33 @@ def _handle_images( return images +def _inject_grammar_for_local_models(llm: BaseChatModel, output_type: type) -> None: + """ + Inject GBNF grammar into local models. + """ + try: + from funcchain._llms import ChatOllama + except: # noqa + pass + else: + if isinstance(llm, ChatOllama): + if isinstance(output_type, UnionType): + raise NotImplementedError( + "Union types are not yet supported for LlamaCpp models." + ) # TODO: implement + + if issubclass(output_type, BaseModel) and not issubclass( + output_type, ParserBaseModel + ): + llm.grammar = pydantic_to_grammar(output_type) + if issubclass(output_type, ParserBaseModel): + llm.grammar = output_type.custom_grammar() + + def _gather_llm( settings: FuncchainSettings, -) -> BaseChatModel | RunnableWithFallbacks: - if isinstance(settings.llm, RunnableWithFallbacks) or isinstance( - settings.llm, BaseChatModel - ): +) -> BaseChatModel: + if isinstance(settings.llm, BaseChatModel): llm = settings.llm else: llm = univeral_model_selector(settings) @@ -271,12 +266,28 @@ def _gather_llm( "No language model provided. Either set the llm environment variable or " "pass a model to the `chain` function." ) + return llm + + +def _add_custom_callbacks( + llm: BaseChatModel, settings: FuncchainSettings +) -> BaseChatModel: + callbacks: Callbacks = [] + if handler := stream_handler.get(): + callbacks = [handler] + + if settings.console_stream: + from ..streaming import AsyncStreamHandler + + callbacks = [ + AsyncStreamHandler(print, {"end": "", "flush": True}), + ] + + if callbacks: settings.streaming = True - if isinstance(llm, RunnableWithFallbacks) and isinstance( - llm.runnable, BaseChatModel - ): - llm.runnable.callbacks = [handler] - elif isinstance(llm, BaseChatModel): - llm.callbacks = [handler] + if hasattr(llm, "streaming"): + llm.streaming = True + llm.callbacks = callbacks + return llm From 5551b306a0e2d396bf4abc4247f71c0f45081d96 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 28 Dec 2023 15:21:15 +0100 Subject: [PATCH 033/451] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20update=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 18 ++++++------------ requirements-dev.lock | 11 ++++------- requirements.lock | 7 ++++--- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58c7d65..00068bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ authors = [ { name = "Shroominic", email = "contact@shroominic.com" } ] dependencies = [ - "langchain>=0.0.347", - "pydantic-settings>=2.1.0", + "langchain[openai]>=0.0.352", + "pydantic-settings>=2", "docstring-parser>=0.15", - "rich>=13.7.0", - "jinja2>=3.1.2", - "pillow>=10.1.0", + "rich>=13", + "jinja2>=3", + "pillow>=10", ] license = "MIT" readme = "README.md" @@ -49,16 +49,10 @@ dev-dependencies = [ ] [project.optional-dependencies] -local = [ - "llama-cpp-python>=0.2.20", - "huggingface_hub>=0.19.4", -] openai = [ - "openai>=1.3.4", - "tiktoken>=0.5.1", + "langchain[openai]", ] all = [ - "funcchain[local]", "funcchain[openai]", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 79434b5..4347df8 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -22,19 +22,16 @@ click==8.1.7 colorama==0.4.6 dataclasses-json==0.6.2 decorator==5.1.1 -diskcache==5.6.3 distlib==0.3.7 distro==1.8.0 docstring-parser==0.15 executing==2.0.1 filelock==3.13.1 frozenlist==1.4.0 -fsspec==2023.12.1 ghp-import==2.1.0 h11==0.14.0 httpcore==1.0.2 httpx==0.25.1 -huggingface-hub==0.19.4 identify==2.5.32 idna==3.4 iniconfig==2.0.0 @@ -44,10 +41,10 @@ jedi==0.19.1 jinja2==3.1.2 jsonpatch==1.33 jsonpointer==2.4 -langchain==0.0.348 -langchain-core==0.0.12 -langsmith==0.0.66 -llama-cpp-python==0.2.20 +langchain==0.0.352 +langchain-community==0.0.6 +langchain-core==0.1.3 +langsmith==0.0.75 markdown==3.5.1 markdown-it-py==3.0.0 markupsafe==2.1.3 diff --git a/requirements.lock b/requirements.lock index 12c03de..42df27e 100644 --- a/requirements.lock +++ b/requirements.lock @@ -21,9 +21,10 @@ idna==3.4 jinja2==3.1.2 jsonpatch==1.33 jsonpointer==2.4 -langchain==0.0.348 -langchain-core==0.0.12 -langsmith==0.0.66 +langchain==0.0.352 +langchain-community==0.0.6 +langchain-core==0.1.3 +langsmith==0.0.75 markdown-it-py==3.0.0 markupsafe==2.1.3 marshmallow==3.20.1 From f2c1323fe57645fb61f1e5f50329ff3269bf2287 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 28 Dec 2023 15:22:04 +0100 Subject: [PATCH 034/451] =?UTF-8?q?=F0=9F=92=AC=20fix=20image=20history=20?= =?UTF-8?q?for=20non=20vision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/creation.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/funcchain/chain/creation.py b/src/funcchain/chain/creation.py index 31dcc0a..6a525c0 100644 --- a/src/funcchain/chain/creation.py +++ b/src/funcchain/chain/creation.py @@ -130,7 +130,7 @@ def create_chain( ) # for vision models - images = _handle_images(llm, input_kwargs) + images = _handle_images(llm, memory, input_kwargs) # create prompts instruction_prompt = create_instruction_prompt( @@ -214,6 +214,7 @@ def _crop_large_inputs( def _handle_images( llm: BaseChatModel, + memory: BaseChatMessageHistory, input_kwargs: dict[str, str], ) -> list[Image.Image]: """ @@ -226,6 +227,9 @@ def _handle_images( del input_kwargs[k] elif images: raise RuntimeError("Images as input are only supported for vision models.") + elif _history_contains_images(memory): + print("Warning: Images in chat history are ignored for non-vision models.") + memory.messages = _clear_images_from_history(memory.messages) return images @@ -291,3 +295,27 @@ def _add_custom_callbacks( llm.callbacks = callbacks return llm + + +def _history_contains_images(history: BaseChatMessageHistory) -> bool: + """ + Check if the chat history contains images. + """ + for message in history.messages: + if isinstance(message.content, list): + for content in message.content: + if isinstance(content, dict) and content.get("type") == "image_url": + return True + return False + + +def _clear_images_from_history(history: list[BaseMessage]) -> list[BaseMessage]: + """ + Remove images from the chat history. + """ + for message in history: + if isinstance(message.content, list): + for content in message.content: + if isinstance(content, dict) and content.get("type") == "image_url": + message.content.remove(content) + return history From c407fae61ed148bfef0ee6659c5e7ef581eb5ce9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 28 Dec 2023 15:23:12 +0100 Subject: [PATCH 035/451] =?UTF-8?q?=F0=9F=92=AC=20fix=20vision=20model=20t?= =?UTF-8?q?ype=20gather?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/helpers.py | 91 ++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/funcchain/utils/helpers.py b/src/funcchain/utils/helpers.py index b03ca1b..6603fb9 100644 --- a/src/funcchain/utils/helpers.py +++ b/src/funcchain/utils/helpers.py @@ -1,10 +1,9 @@ from typing import Any, NoReturn, Type from docstring_parser import parse -from langchain.chat_models import ChatOpenAI -from langchain_core.language_models import BaseChatModel, BaseLanguageModel +from langchain.chat_models import ChatOpenAI, ChatOllama +from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage -from langchain_core.runnables import Runnable, RunnableWithFallbacks from pydantic import BaseModel from tiktoken import encoding_for_model @@ -17,7 +16,7 @@ def count_tokens(text: str, model: str = "gpt-4") -> int: return len(encoding_for_model(model).encode(text)) -verified_function_models = [ +verified_openai_function_models = [ "gpt-4", "gpt-4-0613", "gpt-4-1106-preview", @@ -30,59 +29,67 @@ def count_tokens(text: str, model: str = "gpt-4") -> int: "gpt-3.5-turbo-16k-0613", ] -verified_vision_models = [ +verified_openai_vision_models = [ "gpt-4-vision-preview", ] +verified_ollama_vision_models = [ + "llava", + "bakllava", +] + -def gather_llm_type(llm: BaseLanguageModel | Runnable, func_check: bool = True) -> str: - if isinstance(llm, RunnableWithFallbacks): - llm = llm.runnable +def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: if not isinstance(llm, BaseChatModel): return "base_model" - if not isinstance(llm, ChatOpenAI): - return "chat_model" - if llm.model_name in verified_vision_models: - return "vision_model" - if llm.model_name in verified_function_models: - return "function_model" - try: - if func_check: - llm.predict_messages( - [ - SystemMessage( - content="This is a test message to see if the model can run functions." - ), - HumanMessage(content="Hello!"), - ], - functions=[ - { - "name": "print", - "description": "show the input", - "parameters": { - "properties": { - "__arg1": {"title": "__arg1", "type": "string"}, + if isinstance(llm, ChatOpenAI): + if llm.model_name in verified_openai_vision_models: + return "vision_model" + if llm.model_name in verified_openai_function_models: + return "function_model" + try: + if func_check: + llm.predict_messages( + [ + SystemMessage( + content="This is a test message to see if the model can run functions." + ), + HumanMessage(content="Hello!"), + ], + functions=[ + { + "name": "print", + "description": "show the input", + "parameters": { + "properties": { + "__arg1": {"title": "__arg1", "type": "string"}, + }, + "required": ["__arg1"], + "type": "object", }, - "required": ["__arg1"], - "type": "object", - }, - } - ], - ) - except Exception: - return "chat_model" - else: - return "function_model" + } + ], + ) + except Exception: + return "chat_model" + else: + return "function_model" + elif isinstance(llm, ChatOllama): + for model in verified_ollama_vision_models: + if llm.model in model: + return "vision_model" + + return "chat_model" def is_function_model( - llm: BaseLanguageModel | RunnableWithFallbacks, + llm: BaseChatModel, ) -> bool: return gather_llm_type(llm) == "function_model" def is_vision_model( - llm: BaseLanguageModel | RunnableWithFallbacks, + llm: BaseChatModel, ) -> bool: return gather_llm_type(llm) == "vision_model" From 9d913b8a1f1dc3397f71cc45789c045399c91a3f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 28 Dec 2023 22:16:00 +0100 Subject: [PATCH 036/451] =?UTF-8?q?=F0=9F=94=80=20improve=20chat=20router?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/router_component.py | 5 +- src/funcchain/components.py | 315 ++++++++++++++--------------------- tests/router_test.py | 9 +- 3 files changed, 134 insertions(+), 195 deletions(-) diff --git a/examples/router_component.py b/examples/router_component.py index ce81fb0..532ff95 100644 --- a/examples/router_component.py +++ b/examples/router_component.py @@ -26,7 +26,10 @@ def handle_default_requests(user_query: str) -> str: "handler": handle_csv_requests, "description": "Call this for requests including CSV Files.", }, - "default": handle_default_requests, + "default": { + "handler": handle_default_requests, + "description": "Call this for all other requests.", + }, }, ) diff --git a/src/funcchain/components.py b/src/funcchain/components.py index b02562c..26c3652 100644 --- a/src/funcchain/components.py +++ b/src/funcchain/components.py @@ -1,43 +1,90 @@ from enum import Enum -from typing import Iterator, Union, Callable, Any, TypeVar, Optional, AsyncIterator +from typing import AsyncIterator, Callable, Any, Iterator, TypeVar, Optional from typing_extensions import TypedDict -from langchain_core.pydantic_v1 import validator as field_validator from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.language_models import BaseChatModel -from langchain_core.messages import HumanMessage -from langchain_core.runnables import Runnable, RunnableConfig, RunnableSerializable +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.runnables import ( + Runnable, + RunnableSerializable, + RouterRunnable, + RunnableLambda, +) +from langchain_core.runnables.config import RunnableConfig +from langchain_core.runnables.router import RouterInput from funcchain import runnable -from .utils.msg_tools import msg_to_str +from funcchain.utils.msg_tools import msg_to_str class Route(TypedDict): - handler: Callable + handler: Callable | Runnable description: str -Routes = dict[str, Union[Route, Callable]] +Routes = dict[str, Route] ResponseType = TypeVar("ResponseType") -class ChatRouter(RunnableSerializable[HumanMessage, ResponseType]): - routes: Routes - history: BaseChatMessageHistory | None = None - llm: BaseChatModel | str | None = None +class ChatRouter(RouterRunnable[ResponseType]): + """A router component that can be used to route user requests to different handlers.""" + + def __init__( + self, + *, + routes: Routes, + history: Optional[BaseChatMessageHistory] = None, + llm: Optional[BaseChatModel | str] = None, + add_default: bool = False, + **kwargs: Any, + ) -> None: + super().__init__( + runnables={name: run["handler"] for name, run in routes.items()}, + **kwargs, + ) + self.llm = llm + self.history = history + self.routes = self._add_default_handler(routes) if add_default else routes class Config: arbitrary_types_allowed = True - - @field_validator("routes") - def validate_routes(cls, v: Routes) -> Routes: - if "default" not in v.keys(): - raise ValueError("`default` route is missing") - return v - - def create_route(self) -> Runnable[dict[str, str], Any]: + extra = "allow" + + @classmethod + def create_router( + cls, + *, + routes: Routes, + history: Optional[BaseChatMessageHistory] = None, + llm: Optional[BaseChatModel | str] = None, + **kwargs: Any, + ) -> RunnableSerializable[Any, ResponseType]: + router = cls( + routes=routes, llm=llm, history=history, add_default=True, **kwargs + ) + return { + "input": lambda x: x, + "key": ( + lambda x: { + # "image": x.images[0], + # "user_request": x.__str__(), + "user_request": x.content, + "routes": router._routes_repr(), + } + ) + | cls._selector(routes, llm, history) + | RunnableLambda(lambda x: x.selector.__str__()), + } | router + + @staticmethod + def _selector( + routes: Routes, + llm: BaseChatModel | str | None = None, + history: BaseChatMessageHistory | None = None, + ) -> Runnable[dict[str, str], Any]: RouteChoices = Enum( # type: ignore "RouteChoices", - {r: r for r in self.routes.keys()}, + {r: r for r in routes.keys()}, type=str, ) from pydantic import BaseModel, Field @@ -50,205 +97,95 @@ class RouterModel(BaseModel): return runnable( instruction="Given the user request select the appropriate route.", - input_args=["user_request", "routes"], # todo: optional image + input_args=["user_request", "routes"], # todo: optional images output_type=RouterModel, - context=self.history.messages if self.history else [], - settings_override={"llm": self.llm}, + context=history.messages if history else [], + settings_override={"llm": llm}, ) - def show_routes(self) -> str: + def _add_default_handler(self, routes: Routes) -> Routes: + if "default" not in routes.keys(): + routes["default"] = { + "handler": ( + RunnableLambda(lambda x: msg_to_str(x)) + | runnable( + instruction="{user_request}", + input_args=["user_request"], + output_type=str, + settings_override={"llm": self.llm}, + ) + | RunnableLambda(lambda x: AIMessage(content=x)) + ), + "description": ( + "Choose this for everything else like " + "normal questions or random things.\n" + "As example: 'How does this work?' or " + "'Whatsup' or 'What is the meaning of life?'" + ), + } + return routes + + def _routes_repr(self) -> str: return "\n".join( [ f"{route_name}: {route['description']}" - if isinstance(route, dict) - else f"{route_name}: {route.__name__}" for route_name, route in self.routes.items() ] ) - def invoke_route(self, user_query: str, /, **kwargs: Any) -> ResponseType: - route_query = self.create_route() - - selected_route = route_query.invoke( - input={ - "user_request": user_query, - "routes": self.show_routes(), - }, - config={"run_name": "ChatRouter"}, - ).selector - assert isinstance(selected_route, str) - - if isinstance(self.routes[selected_route], dict): - return self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - return self.routes[selected_route](user_query, **kwargs) # type: ignore - - async def ainvoke_route(self, user_query: str, /, **kwargs: Any) -> Any: - import asyncio + def post_update_history(self, input: RouterInput, output: ResponseType) -> None: + input = input["input"] + if self.history: + if isinstance(input, HumanMessage): + self.history.add_message(input) + if isinstance(output, AIMessage): + self.history.add_message(output) - if not all( - [ - asyncio.iscoroutinefunction(route["handler"]) - if isinstance(route, dict) - else asyncio.iscoroutinefunction(route) - for route in self.routes.values() - ] - ): - raise ValueError("All routes must be awaitable when using `ainvoke_route`") + # TODO: deprecate + def invoke_route(self, user_query: str, /, **kwargs: Any) -> ResponseType: + """Deprecated. Use invoke instead.""" + route_query = self._selector(self.routes) - route_query = self.create_route() selected_route = route_query.invoke( - input={ - "user_request": user_query, - "routes": self.show_routes(), - }, - config={"run_name": "ChatRouter"}, + input={"user_request": user_query, "routes": self._routes_repr()} ).selector - assert isinstance(selected_route, str) - - if isinstance(self.routes[selected_route], dict): - return await self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - return await self.routes[selected_route](user_query, **kwargs) # type: ignore + return self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore def invoke( - self, - input: HumanMessage, - config: Optional[RunnableConfig] = None, - **kwargs: Any, + self, input: RouterInput, config: RunnableConfig | None = None ) -> ResponseType: - user_query = msg_to_str(input) - route_query = self.create_route() - - selected_route = route_query.invoke( - input={ - "user_request": user_query, - "routes": self.show_routes(), - }, - config=config.update({"run_name": "ChatRouter"}) if config else None, - ).selector - assert isinstance(selected_route, str) - - if isinstance(self.routes[selected_route], dict): - if hasattr(self.routes[selected_route]["handler"], "invoke"): # type: ignore - return self.routes[selected_route]["handler"].invoke( # type: ignore - input, config, **kwargs - ) - return self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - - if hasattr(self.routes[selected_route], "invoke"): - return self.routes[selected_route].invoke(input, config, **kwargs) # type: ignore - return self.routes[selected_route](user_query, **kwargs) # type: ignore + output = super().invoke(input, config) + self.post_update_history(input, output) + return output async def ainvoke( self, - input: HumanMessage, - config: Optional[RunnableConfig] = None, - **kwargs: Any, + input: RouterInput, + config: RunnableConfig | None = None, + **kwargs: Any | None, ) -> ResponseType: - user_query = msg_to_str(input) - import asyncio - - if not all( - [ - asyncio.iscoroutinefunction(route["handler"]) - if isinstance(route, dict) - else asyncio.iscoroutinefunction(route) - for route in self.routes.values() - ] - ): - raise ValueError("All routes must be awaitable when using `ainvoke_route`") - - route_query = self.create_route() - selected_route = route_query.invoke( - input={ - "user_request": user_query, - "routes": self.show_routes(), - }, - config=config.update({"run_name": "ChatRouter"}) if config else None, - ).selector - assert isinstance(selected_route, str) - - if isinstance(self.routes[selected_route], dict): - if hasattr(self.routes[selected_route]["handler"], "ainvoke"): # type: ignore - return await self.routes[selected_route]["handler"].ainvoke( # type: ignore - input, config, **kwargs - ) - return await self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - - if hasattr(self.routes[selected_route], "ainvoke"): - return await self.routes[selected_route].ainvoke(input, config, **kwargs) # type: ignore - return await self.routes[selected_route](user_query, **kwargs) # type: ignore + output = await super().ainvoke(input, config, **kwargs) + self.post_update_history(input, output) + return output def stream( self, - input: HumanMessage, + input: RouterInput, config: RunnableConfig | None = None, **kwargs: Any | None, ) -> Iterator[ResponseType]: - user_query = msg_to_str(input) - route_query = self.create_route() - - selected_route = route_query.invoke( - input={ - "user_request": user_query, - "routes": self.show_routes(), - }, - config=config.update({"run_name": "ChatRouter"}) if config else None, - ).selector - assert isinstance(selected_route, str) - - if isinstance(self.routes[selected_route], dict): - if hasattr(self.routes[selected_route]["handler"], "stream"): # type: ignore - yield from self.routes[selected_route]["handler"].stream( # type: ignore - input, config, **kwargs - ) - yield self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - - if hasattr(self.routes[selected_route], "stream"): - yield from self.routes[selected_route].stream(input, config, **kwargs) # type: ignore - yield self.routes[selected_route](user_query, **kwargs) # type: ignore + for i in super().stream(input, config, **kwargs): + yield (last := i) + if last: + self.post_update_history(input, last) async def astream( self, - input: HumanMessage, + input: RouterInput, config: RunnableConfig | None = None, **kwargs: Any | None, ) -> AsyncIterator[ResponseType]: - user_query = msg_to_str(input) - import asyncio - - if not all( - [ - asyncio.iscoroutinefunction(route["handler"]) - if isinstance(route, dict) - else asyncio.iscoroutinefunction(route) - for route in self.routes.values() - ] - ): - raise ValueError("All routes must be awaitable when using `ainvoke_route`") - - route_query = self.create_route() - selected_route = ( - await route_query.ainvoke( - input={ - "user_request": user_query, - "routes": self.show_routes(), - }, - config=config.update({"run_name": "ChatRouter"}) if config else None, - ) - ).selector - assert isinstance(selected_route, str) - - if isinstance(self.routes[selected_route], dict): - if hasattr(self.routes[selected_route]["handler"], "astream"): # type: ignore - async for item in self.routes[selected_route]["handler"].astream( # type: ignore - input, config, **kwargs - ): - yield item - yield await self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - - if hasattr(self.routes[selected_route], "astream"): - async for item in self.routes[selected_route].astream( # type: ignore - input, config, **kwargs - ): - yield item - yield await self.routes[selected_route](user_query, **kwargs) # type: ignore + async for ai in super().astream(input, config, **kwargs): + yield (last := ai) + if last: + self.post_update_history(input, last) diff --git a/tests/router_test.py b/tests/router_test.py index 4b4b5b4..f230bce 100644 --- a/tests/router_test.py +++ b/tests/router_test.py @@ -9,10 +9,6 @@ def handle_csv_requests(user_query: str) -> str: return f"Handling CSV requests with user query: {user_query}" -def handle_default_requests(user_query: str) -> str: - return f"Handling DEFAULT requests with user query: {user_query}" - - router = ChatRouter[str]( routes={ "pdf": { @@ -23,7 +19,10 @@ def handle_default_requests(user_query: str) -> str: "handler": handle_csv_requests, "description": "Call this for requests including CSV Files.", }, - "default": handle_default_requests, + "default": { + "handler": lambda x: f"Handling DEFAULT with user query: {x}", + "description": "Call this for all other requests.", + }, }, ) From c6acd1725f65ccc49a9c477a39096c5b15083b7e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 29 Dec 2023 13:47:55 +0100 Subject: [PATCH 037/451] =?UTF-8?q?=E2=9C=A8=20Add=20dynamic=20chat=20rout?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/dynamic_router.py | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 examples/dynamic_router.py diff --git a/examples/dynamic_router.py b/examples/dynamic_router.py new file mode 100644 index 0000000..7bb574e --- /dev/null +++ b/examples/dynamic_router.py @@ -0,0 +1,88 @@ +from enum import Enum +from typing import Callable, TypedDict, Any +from pydantic import BaseModel, Field +from funcchain import runnable + +# Dynamic Router Definition: + + +class Route(TypedDict): + handler: Callable + description: str + + +class DynamicChatRouter(BaseModel): + routes: dict[str, Route] + + def _routes_repr(self) -> str: + return "\n".join( + [ + f"{route_name}: {route['description']}" + for route_name, route in self.routes.items() + ] + ) + + def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) + + class RouterModel(BaseModel): + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) + + route_query = runnable( + instruction="Given the user query select the best query handler for it.", + input_args=["user_query", "query_handlers"], + output_type=RouterModel, + ) + + selected_route = route_query.invoke( + input={ + "user_query": user_query, + "query_handlers": self._routes_repr(), + } + ).selector + assert isinstance(selected_route, str) + + return self.routes[selected_route]["handler"](user_query, **kwargs) + + +# Example Usage: + + +def handle_pdf_requests(user_query: str) -> str: + return "Handling PDF requests with user query: " + user_query + + +def handle_csv_requests(user_query: str) -> str: + return "Handling CSV requests with user query: " + user_query + + +def handle_default_requests(user_query: str) -> str: + return "Handling DEFAULT requests with user query: " + user_query + + +router = DynamicChatRouter( + routes={ + "pdf": { + "handler": handle_pdf_requests, + "description": "Call this for requests including PDF Files.", + }, + "csv": { + "handler": handle_csv_requests, + "description": "Call this for requests including CSV Files.", + }, + "default": { + "handler": handle_default_requests, + "description": "Call this for all other requests.", + }, + }, +) + + +router.invoke_route("Can you summarize this csv?") From c028e4d2bd2a4570b5afb177932eba255806b2a9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 29 Dec 2023 13:47:55 +0100 Subject: [PATCH 038/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20router?= =?UTF-8?q?=5Fcomponent.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/router_component.py | 37 ------------------------------------ 1 file changed, 37 deletions(-) delete mode 100644 examples/router_component.py diff --git a/examples/router_component.py b/examples/router_component.py deleted file mode 100644 index 532ff95..0000000 --- a/examples/router_component.py +++ /dev/null @@ -1,37 +0,0 @@ -from funcchain.components import ChatRouter -from funcchain.settings import settings - -settings.llm = "ollama/openchat" - - -def handle_pdf_requests(user_query: str) -> str: - return "Handling PDF requests with user query: " + user_query - - -def handle_csv_requests(user_query: str) -> str: - return "Handling CSV requests with user query: " + user_query - - -def handle_default_requests(user_query: str) -> str: - return "Handling DEFAULT requests with user query: " + user_query - - -router = ChatRouter[str]( - routes={ - "pdf": { - "handler": handle_pdf_requests, - "description": "Call this for requests including PDF Files.", - }, - "csv": { - "handler": handle_csv_requests, - "description": "Call this for requests including CSV Files.", - }, - "default": { - "handler": handle_default_requests, - "description": "Call this for all other requests.", - }, - }, -) - - -router.invoke_route("Can you summarize this csv?") From cd767406e70bc6dd9b900e48fbacdc5695dc4a18 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 29 Dec 2023 13:47:55 +0100 Subject: [PATCH 039/451] =?UTF-8?q?=F0=9F=94=81=20Rename=20static=5Frouter?= =?UTF-8?q?.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/static_router.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 examples/static_router.py diff --git a/examples/static_router.py b/examples/static_router.py new file mode 100644 index 0000000..fe0f4a9 --- /dev/null +++ b/examples/static_router.py @@ -0,0 +1,57 @@ +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field + +from funcchain import chain, settings + +settings.console_stream = True +# settings.llm = "ollama/openhermes2.5-mistral" + + +def handle_pdf_requests( + user_query: str, +) -> None: + print("Handling PDF requests with user query: ", user_query) + + +def handle_csv_requests(user_query: str) -> None: + print("Handling CSV requests with user query: ", user_query) + + +def handle_default_requests(user_query: str) -> Any: + print("Handling DEFAULT requests with user query: ", user_query) + + +class RouteChoices(str, Enum): + pdf = "pdf" + csv = "csv" + default = "default" + + +class Router(BaseModel): + selector: RouteChoices = Field(description="Enum of the available routes.") + + def invoke_route(self, user_query: str) -> Any: + match self.selector.value: + case RouteChoices.pdf: + return handle_pdf_requests(user_query) + case RouteChoices.csv: + return handle_csv_requests(user_query) + case RouteChoices.default: + return handle_default_requests(user_query) + + +def route_query(user_query: str) -> Router: + """ + Given a user query select the best query handler for it. + """ + return chain() + + +if __name__ == "__main__": + user_query = input("Enter your query: ") + + routed_chain = route_query(user_query) + + routed_chain.invoke_route(user_query) From b3290d5b9eacef4b63788978c44e5fdcd23f7f1b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 29 Dec 2023 13:47:55 +0100 Subject: [PATCH 040/451] =?UTF-8?q?=F0=9F=94=A7=20Adjust=20property=20enum?= =?UTF-8?q?eration=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/grammars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/utils/grammars.py b/src/funcchain/utils/grammars.py index e41e967..7f48d86 100644 --- a/src/funcchain/utils/grammars.py +++ b/src/funcchain/utils/grammars.py @@ -127,7 +127,7 @@ def format_grammar(self) -> str: def schema_to_grammar(json_schema: dict) -> str: schema = json_schema - prop_order = {name: idx for idx, name in enumerate(schema.keys())} + prop_order = {name: idx for idx, name in enumerate(schema["properties"].keys())} defs = schema.get("$defs", {}) converter = SchemaConverter(prop_order, defs) converter.visit(schema, "") From eb83456efc881feb8ffbd06c37837b917972663f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 29 Dec 2023 13:47:55 +0100 Subject: [PATCH 041/451] =?UTF-8?q?=F0=9F=A7=AA=20Re-enable=20all=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/openai_test.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/tests/openai_test.py b/tests/openai_test.py index 2927800..98f41df 100644 --- a/tests/openai_test.py +++ b/tests/openai_test.py @@ -59,20 +59,7 @@ def analyse(image: Image.Image) -> Analysis: ) -def test_api_key_failure() -> None: - settings.llm = "gpt-3.5-turbo-1106" - settings.openai_api_key = "test" - - try: - print(todo_list("software engineer")) - except Exception: - assert True - else: - assert False, "API Key failure did not occur." - - if __name__ == "__main__": - # test_gpt_35_turbo() - # test_gpt4() - # test_vision() - test_api_key_failure() + test_gpt_35_turbo() + test_gpt4() + test_vision() From 4284b3a50e8fd3a90826d879e53eb741ea5f2e10 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 29 Dec 2023 13:47:55 +0100 Subject: [PATCH 042/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20router=5Ftest.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/router_test.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 tests/router_test.py diff --git a/tests/router_test.py b/tests/router_test.py deleted file mode 100644 index f230bce..0000000 --- a/tests/router_test.py +++ /dev/null @@ -1,43 +0,0 @@ -from funcchain.components import ChatRouter - - -def handle_pdf_requests(user_query: str) -> str: - return f"Handling PDF requests with user query: {user_query}" - - -def handle_csv_requests(user_query: str) -> str: - return f"Handling CSV requests with user query: {user_query}" - - -router = ChatRouter[str]( - routes={ - "pdf": { - "handler": handle_pdf_requests, - "description": "Call this for requests including PDF Files.", - }, - "csv": { - "handler": handle_csv_requests, - "description": "Call this for requests including CSV Files.", - }, - "default": { - "handler": lambda x: f"Handling DEFAULT with user query: {x}", - "description": "Call this for all other requests.", - }, - }, -) - - -def test_router() -> None: - assert "Handling CSV" in router.invoke_route("Can you summarize this csv?") - - assert "Handling PDF" in router.invoke_route("Can you summarize this pdf?") - - assert "Handling DEFAULT" in router.invoke_route("whatsup") - - assert "Handling DEFAULT" in router.invoke_route( - "I want to book a flight how to do this?" - ) - - -if __name__ == "__main__": - test_router() From bab720063250c3e45898db31d290aa29a8a077da Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:29 +0100 Subject: [PATCH 043/451] =?UTF-8?q?=F0=9F=94=A5=20Simplify=20answer=20rank?= =?UTF-8?q?ing=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/async/expert_answer.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/async/expert_answer.py b/examples/async/expert_answer.py index 09a9d93..612debf 100644 --- a/examples/async/expert_answer.py +++ b/examples/async/expert_answer.py @@ -2,9 +2,8 @@ from asyncio import run as _await from random import shuffle -from pydantic import BaseModel - from funcchain import achain, settings +from pydantic import BaseModel settings.temperature = 1 settings.llm = "openai/gpt-3.5-turbo-1106" @@ -33,14 +32,10 @@ async def expert_answer(question: str) -> str: # Shuffle the answers to ensure randomness enum_answers = list(enumerate(answers)) shuffle(enum_answers) - ranked_answers = await gather( - *(rank_answers(question, enum_answers) for _ in range(3)) - ) + ranked_answers = await gather(*(rank_answers(question, enum_answers) for _ in range(3))) highest_ranked_answer = max( ranked_answers, - key=lambda x: sum( - 1 for ans in ranked_answers if ans.selected_answer == x.selected_answer - ), + key=lambda x: sum(1 for ans in ranked_answers if ans.selected_answer == x.selected_answer), ).selected_answer return answers[highest_ranked_answer] From 3d74b26c548fa8536403c7a3694be45a7cbd8955 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 044/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/async/startup_names.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/async/startup_names.py b/examples/async/startup_names.py index a0be125..d2e8bdd 100644 --- a/examples/async/startup_names.py +++ b/examples/async/startup_names.py @@ -1,8 +1,7 @@ import asyncio -from pydantic import BaseModel - from funcchain import achain, settings +from pydantic import BaseModel settings.temperature = 1 From 85971ad87551440717b54dadccee667033da958c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 045/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/chatgpt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/chatgpt.py b/examples/chatgpt.py index c82953d..0bc3340 100644 --- a/examples/chatgpt.py +++ b/examples/chatgpt.py @@ -1,10 +1,9 @@ """ Simple chatgpt rebuild with memory/history. """ -from langchain.memory import ChatMessageHistory - from funcchain import chain, settings -from funcchain.streaming import stream_to +from funcchain.backend.streaming import stream_to +from langchain.memory import ChatMessageHistory settings.llm = "openai/gpt-4" From 48ea57e354f43fd906c22fa3d31c593513ebaf23 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 046/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20dynamic=5Frou?= =?UTF-8?q?ter.py=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/dynamic_router.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/dynamic_router.py b/examples/dynamic_router.py index 7bb574e..41f4187 100644 --- a/examples/dynamic_router.py +++ b/examples/dynamic_router.py @@ -1,7 +1,8 @@ from enum import Enum -from typing import Callable, TypedDict, Any -from pydantic import BaseModel, Field +from typing import Any, Callable, TypedDict + from funcchain import runnable +from pydantic import BaseModel, Field # Dynamic Router Definition: @@ -15,12 +16,7 @@ class DynamicChatRouter(BaseModel): routes: dict[str, Route] def _routes_repr(self) -> str: - return "\n".join( - [ - f"{route_name}: {route['description']}" - for route_name, route in self.routes.items() - ] - ) + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: RouteChoices = Enum( # type: ignore From e5dded77fe7f2c0256d21cb6ddf2aab4818184b5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 047/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/enums.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/enums.py b/examples/enums.py index 2304a27..d562960 100644 --- a/examples/enums.py +++ b/examples/enums.py @@ -1,8 +1,7 @@ from enum import Enum -from pydantic import BaseModel - from funcchain import chain, settings +from pydantic import BaseModel class Answer(str, Enum): From 6a277ca3c8a5ac7166f25aba98e75e9e09f304c8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 048/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20import=20and?= =?UTF-8?q?=20whitespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/error_output.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/error_output.py b/examples/error_output.py index 45d8ebe..d805f86 100644 --- a/examples/error_output.py +++ b/examples/error_output.py @@ -1,6 +1,5 @@ -from rich import print - from funcchain import BaseModel, Error, chain +from rich import print class User(BaseModel): @@ -19,6 +18,4 @@ def extract_user_info(text: str) -> User | Error: if __name__ == "__main__": print(extract_user_info("hey")) # returns Error - print( - extract_user_info("I'm John and my mail is john@gmail.com") - ) # returns a User object + print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object From 83eb8fbd2a3efaa326ccadebdbd7046520e6dc9f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 049/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/dynamic_model_generation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/experiments/dynamic_model_generation.py b/examples/experiments/dynamic_model_generation.py index 4b0ce4b..ee1dff6 100644 --- a/examples/experiments/dynamic_model_generation.py +++ b/examples/experiments/dynamic_model_generation.py @@ -1,10 +1,9 @@ +from funcchain import chain, settings +from funcchain.syntax.types import CodeBlock from langchain.document_loaders import WebBaseLoader from pydantic import BaseModel from rich import print -from funcchain import chain, settings -from funcchain.parser import CodeBlock - settings.llm = "gpt-4-1106-preview" settings.context_lenght = 4096 * 8 From 5d18e9af070e080f63827f8266aba1d9d4c4619c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 050/451] =?UTF-8?q?=F0=9F=93=90=20Refactor=20subprocess=20?= =?UTF-8?q?call=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/email_answering.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/experiments/email_answering.py b/examples/experiments/email_answering.py index d7b1071..a1d2416 100644 --- a/examples/experiments/email_answering.py +++ b/examples/experiments/email_answering.py @@ -20,9 +20,7 @@ def get_emails_from_inbox() -> List[Tuple[str, str]]: """ # Run AppleScript and collect output - process = subprocess.Popen( - ["osascript", "-e", apple_script], stdout=subprocess.PIPE - ) + process = subprocess.Popen(["osascript", "-e", apple_script], stdout=subprocess.PIPE) out, _ = process.communicate() raw_output = out.decode("utf-8").strip() From 31c926be8490c9cebad63d475ac8a2a7f7ba8656 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 051/451] =?UTF-8?q?=F0=9F=93=9D=20Streamline=20input=20pro?= =?UTF-8?q?mpts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/generate_pp_and_tos.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/experiments/generate_pp_and_tos.py b/examples/experiments/generate_pp_and_tos.py index 32dc84d..b739521 100644 --- a/examples/experiments/generate_pp_and_tos.py +++ b/examples/experiments/generate_pp_and_tos.py @@ -56,18 +56,14 @@ def generate_pp(answered_questions: list[str]) -> str: if __name__ == "__main__": - print( - "Please answer the following questions to generate a Terms of Service and Privacy Policy." - ) + print("Please answer the following questions to generate a Terms of Service and Privacy Policy.") print("To skip a question, press enter without typing anything.") legal_questions = example_legal_questions.copy() # or from scratch using generate_legal_questions() for i, question in enumerate(legal_questions): - answer = ( - input(f"{i+1}/{len(legal_questions)}: {question} ") or "No answer provided." - ) + answer = input(f"{i+1}/{len(legal_questions)}: {question} ") or "No answer provided." legal_questions[i] = f"Q: {question}\nA: {answer}\n" From 307470635eb71bf4ef6404d4d42f3e6ea61f6716 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 052/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/local_codeblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/experiments/local_codeblock.py b/examples/experiments/local_codeblock.py index 0339c3f..964c450 100644 --- a/examples/experiments/local_codeblock.py +++ b/examples/experiments/local_codeblock.py @@ -1,5 +1,5 @@ from funcchain import chain, settings -from funcchain.types import CodeBlock +from funcchain.syntax.types import CodeBlock def generate_code(instruction: str) -> CodeBlock: From 657022fd1d907d3c2b681a3a363bcaadf920e073 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 053/451] =?UTF-8?q?=F0=9F=A7=B9=20Refactor=20layout=20upda?= =?UTF-8?q?tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../experiments/parallel_console_streaming.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/examples/experiments/parallel_console_streaming.py b/examples/experiments/parallel_console_streaming.py index 10dc354..8973316 100644 --- a/examples/experiments/parallel_console_streaming.py +++ b/examples/experiments/parallel_console_streaming.py @@ -3,15 +3,14 @@ from typing import AsyncGenerator from uuid import uuid4 +from funcchain import achain, settings +from funcchain.backend.streaming import astream_to +from funcchain.utils.funcs import count_tokens from rich.console import Console from rich.layout import Layout from rich.live import Live from rich.panel import Panel -from funcchain import achain, settings -from funcchain.streaming import astream_to -from funcchain.utils import count_tokens - class RenderChain: def __init__(self, renderer: "Renderer", name: str) -> None: @@ -39,9 +38,7 @@ def add_chain(self, chain: RenderChain) -> None: if not self.live.is_started: self.live.start() self.console.height = (len(self.layout.children) + 1) * self.column_height - self.layout.split_column( - *self.layout.children, Layout(name=chain.id, size=self.column_height) - ) + self.layout.split_column(*self.layout.children, Layout(name=chain.id, size=self.column_height)) self.chains.append(chain) def render_stream(self, token: str, chain: RenderChain) -> None: @@ -49,9 +46,7 @@ def render_stream(self, token: str, chain: RenderChain) -> None: tokens: int = 0 max_width: int = self.console.width content_width: int = 0 - if isinstance(panel := self.layout[chain.id]._renderable, Panel) and isinstance( - panel.renderable, str - ): + if isinstance(panel := self.layout[chain.id]._renderable, Panel) and isinstance(panel.renderable, str): content_width = self.console.measure(panel.renderable).maximum if isinstance(panel.title, str) and " " in panel.title: tokens = int(panel.title.split(" ")[1]) @@ -63,16 +58,12 @@ def render_stream(self, token: str, chain: RenderChain) -> None: prev += token else: prev += token - self.layout[chain.id].update( - Panel(prev, title=f"({chain.name}) {tokens} tokens") - ) + self.layout[chain.id].update(Panel(prev, title=f"({chain.name}) {tokens} tokens")) self.live.update(self.layout) def remove(self, chain: RenderChain) -> None: self.chains.remove(chain) - self.layout.split_column( - *(child for child in self.layout.children if child.name != chain.id) - ) + self.layout.split_column(*(child for child in self.layout.children if child.name != chain.id)) self.console.height = (len(self.layout.children)) * self.column_height self.live.update(self.layout) if not self.chains: From 046aa6c98cd1f9211a87dd9ab07481aa0730e9b6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 054/451] =?UTF-8?q?=F0=9F=8D=8E=20Add=20fruit=20salad=20ca?= =?UTF-8?q?lculator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/fruitsalad.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/fruitsalad.py diff --git a/examples/fruitsalad.py b/examples/fruitsalad.py new file mode 100644 index 0000000..4dac483 --- /dev/null +++ b/examples/fruitsalad.py @@ -0,0 +1,19 @@ +from funcchain import chain +from pydantic import BaseModel + + +class FruitSalad(BaseModel): + bananas: int = 0 + apples: int = 0 + + +def sum_fruits(fruit_salad: FruitSalad) -> int: + """ + Sum the number of fruits in a fruit salad. + """ + return chain() + + +def test_fruit_salad() -> None: + fruit_salad = FruitSalad(bananas=3, apples=5) + assert sum_fruits(fruit_salad) == 8 From 7356f179696d0018910ab8ece24ed17718f03909 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 055/451] =?UTF-8?q?=F0=9F=A7=B9=20Add=20whitespace=20for?= =?UTF-8?q?=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/literals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/literals.py b/examples/literals.py index 54f9277..2d0c449 100644 --- a/examples/literals.py +++ b/examples/literals.py @@ -1,4 +1,5 @@ from typing import Literal + from funcchain import chain from pydantic import BaseModel From 79d55a8e9fdde77c022aa5911f8cedd84223bead Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 056/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20ollama.py=20i?= =?UTF-8?q?mports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/ollama.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/ollama.py b/examples/ollama.py index 0b76ca1..12606be 100644 --- a/examples/ollama.py +++ b/examples/ollama.py @@ -1,7 +1,6 @@ -from pydantic import BaseModel, Field - from funcchain import chain, settings -from funcchain.streaming import stream_to +from pydantic import BaseModel, Field +from rich import print # define your model @@ -21,13 +20,11 @@ def analyze(text: str) -> SentimentAnalysis: if __name__ == "__main__": # set global llm settings.llm = "ollama/wizardcoder:34b-python-q3_K_M" - # log tokens as stream to console - with stream_to(print): - # run prompt - poem = analyze("I really like when my dog does a trick!") + settings.console_stream = True - # print final parsed output - from rich import print + # run prompt + poem = analyze("I really like when my dog does a trick!") + # show final parsed output print(poem) From 26e290931b0ba7fd276e2339c39c1aa81777a933 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 057/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20imports=20and?= =?UTF-8?q?=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pydantic_validation.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/pydantic_validation.py b/examples/pydantic_validation.py index 9fb3d33..7dea97b 100644 --- a/examples/pydantic_validation.py +++ b/examples/pydantic_validation.py @@ -1,9 +1,8 @@ -from pydantic import BaseModel, field_validator - from funcchain import chain, settings -from funcchain.streaming import stream_to +from funcchain.backend.streaming import stream_to +from pydantic import BaseModel, field_validator -settings.llm = "ollama/dolphin-2.5-mixtral-8x7b:Q3_K_M" +settings.llm = "ollama/openchat" class Task(BaseModel): From b83f1a4e825bb9b580754537dcde8ef94b7a38b8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 058/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20router=5Fchain.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/router_chain.py | 57 ---------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 examples/router_chain.py diff --git a/examples/router_chain.py b/examples/router_chain.py deleted file mode 100644 index fe0f4a9..0000000 --- a/examples/router_chain.py +++ /dev/null @@ -1,57 +0,0 @@ -from enum import Enum -from typing import Any - -from pydantic import BaseModel, Field - -from funcchain import chain, settings - -settings.console_stream = True -# settings.llm = "ollama/openhermes2.5-mistral" - - -def handle_pdf_requests( - user_query: str, -) -> None: - print("Handling PDF requests with user query: ", user_query) - - -def handle_csv_requests(user_query: str) -> None: - print("Handling CSV requests with user query: ", user_query) - - -def handle_default_requests(user_query: str) -> Any: - print("Handling DEFAULT requests with user query: ", user_query) - - -class RouteChoices(str, Enum): - pdf = "pdf" - csv = "csv" - default = "default" - - -class Router(BaseModel): - selector: RouteChoices = Field(description="Enum of the available routes.") - - def invoke_route(self, user_query: str) -> Any: - match self.selector.value: - case RouteChoices.pdf: - return handle_pdf_requests(user_query) - case RouteChoices.csv: - return handle_csv_requests(user_query) - case RouteChoices.default: - return handle_default_requests(user_query) - - -def route_query(user_query: str) -> Router: - """ - Given a user query select the best query handler for it. - """ - return chain() - - -if __name__ == "__main__": - user_query = input("Enter your query: ") - - routed_chain = route_query(user_query) - - routed_chain.invoke_route(user_query) From 310bf05bc7c4ce8932ffd83d921200f2aa03a4a0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 059/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20default=20setti?= =?UTF-8?q?ngs,=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/simple/gather_infos.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/simple/gather_infos.py b/examples/simple/gather_infos.py index 461d6f4..2e833ca 100644 --- a/examples/simple/gather_infos.py +++ b/examples/simple/gather_infos.py @@ -1,6 +1,8 @@ +from funcchain import chain, settings from pydantic import BaseModel -from funcchain import chain +settings.llm = "ollama/openchat" +settings.console_stream = True class Task(BaseModel): @@ -25,7 +27,7 @@ def plan_task(task: Task) -> str: def main() -> None: - task_input = input("\nEnter task input: ") + task_input = "I need to buy apples, oranges and bananas from whole foods" task = gather_infos(task_input) From 78a2ab89d149ac1e002804e13883dc868be8ceb1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 060/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/simple/task_comparison.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/simple/task_comparison.py b/examples/simple/task_comparison.py index 972e463..b3611dd 100644 --- a/examples/simple/task_comparison.py +++ b/examples/simple/task_comparison.py @@ -1,8 +1,7 @@ import asyncio -from pydantic import BaseModel - from funcchain import achain, chain +from pydantic import BaseModel class Task(BaseModel): From 82727450738ada6a044b7ec5c83b516b27e9707f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 061/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/simple/tutorial.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/simple/tutorial.py b/examples/simple/tutorial.py index bb39f68..6eb35f7 100644 --- a/examples/simple/tutorial.py +++ b/examples/simple/tutorial.py @@ -1,7 +1,6 @@ -from pydantic import BaseModel, Field, validator - # %% from funcchain import chain +from pydantic import BaseModel, Field, validator # %% From dfcf5b435ff829af9d9bff89cddcd80f59fd34c5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 062/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20imports=20in?= =?UTF-8?q?=20static=5Frouter.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/static_router.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/static_router.py b/examples/static_router.py index fe0f4a9..5af3576 100644 --- a/examples/static_router.py +++ b/examples/static_router.py @@ -1,9 +1,8 @@ from enum import Enum from typing import Any -from pydantic import BaseModel, Field - from funcchain import chain, settings +from pydantic import BaseModel, Field settings.console_stream = True # settings.llm = "ollama/openhermes2.5-mistral" From 100daa48d3bf4533fb16a3a235b242a4742e66ac Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 063/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/stream.py b/examples/stream.py index 9c8be06..c2dac4f 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -1,5 +1,5 @@ from funcchain import chain, settings -from funcchain.streaming import stream_to +from funcchain.backend.streaming import stream_to settings.temperature = 1 From 14021cbcad3fd8a9ccc2fc70a791e2e246848feb Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 064/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/union_types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/union_types.py b/examples/union_types.py index 5c16b5b..8dc6afc 100644 --- a/examples/union_types.py +++ b/examples/union_types.py @@ -1,6 +1,5 @@ -from pydantic import BaseModel, Field - from funcchain import chain +from pydantic import BaseModel, Field class Item(BaseModel): From c18b6af0803dd1027e2f1fdd6c300808870e8a62 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:30 +0100 Subject: [PATCH 065/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20import=20stat?= =?UTF-8?q?ements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/vision.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/vision.py b/examples/vision.py index 972c8c5..b91967a 100644 --- a/examples/vision.py +++ b/examples/vision.py @@ -1,8 +1,7 @@ +from funcchain import chain, settings from PIL import Image from pydantic import BaseModel, Field -from funcchain import chain, settings - settings.llm = "openai/gpt-4-vision-preview" @@ -24,7 +23,7 @@ def analyse_image(image: Image.Image) -> AnalysisResult: if __name__ == "__main__": example_image = Image.open("examples/assets/old_chinese_temple.jpg") - from funcchain.streaming import stream_to + from funcchain.backend.streaming import stream_to with stream_to(print): result = analyse_image(example_image) From 8865dd0c9e1dac5fd456980ee0df6edf3917f6c5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:50 +0100 Subject: [PATCH 066/451] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20up=20async=5Ftes?= =?UTF-8?q?t.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/async_test.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/async_test.py b/tests/async_test.py index e4cb787..5f69e81 100644 --- a/tests/async_test.py +++ b/tests/async_test.py @@ -2,9 +2,8 @@ from asyncio import run as _await from random import shuffle -from pydantic import BaseModel - from funcchain import achain, settings +from pydantic import BaseModel settings.temperature = 1 settings.llm = "openai/gpt-3.5-turbo-1106" @@ -40,14 +39,10 @@ async def expert_answer( # Shuffle the answers to ensure randomness enum_answers = list(enumerate(answers)) shuffle(enum_answers) - ranked_answers = await gather( - *(rank_answers(question, enum_answers) for _ in range(3)) - ) + ranked_answers = await gather(*(rank_answers(question, enum_answers) for _ in range(3))) highest_ranked_answer = max( ranked_answers, - key=lambda x: sum( - 1 for ans in ranked_answers if ans.selected_answer == x.selected_answer - ), + key=lambda x: sum(1 for ans in ranked_answers if ans.selected_answer == x.selected_answer), ).selected_answer return answers[highest_ranked_answer] From 273acca6488472e4087383f5874ac83345ad065f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:50 +0100 Subject: [PATCH 067/451] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20up=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ollama_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ollama_test.py b/tests/ollama_test.py index 6c6f7f3..f6733d1 100644 --- a/tests/ollama_test.py +++ b/tests/ollama_test.py @@ -1,7 +1,6 @@ import pytest -from pydantic import BaseModel - from funcchain import chain, settings +from pydantic import BaseModel class Task(BaseModel): From f134e184a69f3ca4c4c7f7ace2e19e8059dd8887 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:50 +0100 Subject: [PATCH 068/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/openai_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/openai_test.py b/tests/openai_test.py index 98f41df..9644427 100644 --- a/tests/openai_test.py +++ b/tests/openai_test.py @@ -1,6 +1,5 @@ -from pydantic import BaseModel, Field - from funcchain import chain, settings +from pydantic import BaseModel, Field class Task(BaseModel): From f0fb9318d34cb7a4a2567344c619ba2432f8d2d0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:50 +0100 Subject: [PATCH 069/451] =?UTF-8?q?=E2=9C=A8=20Add=20retry=5Fparsing=5Ftes?= =?UTF-8?q?t=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/retry_parsing_test.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/retry_parsing_test.py diff --git a/tests/retry_parsing_test.py b/tests/retry_parsing_test.py new file mode 100644 index 0000000..044a482 --- /dev/null +++ b/tests/retry_parsing_test.py @@ -0,0 +1 @@ +# todo From 0cf4b57adfac1040c4f6e896b77fb3a247d4a671 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:52:50 +0100 Subject: [PATCH 070/451] =?UTF-8?q?=E2=9C=A8=20Add=20router=5Ftest=20place?= =?UTF-8?q?holder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/router_test.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/router_test.py diff --git a/tests/router_test.py b/tests/router_test.py new file mode 100644 index 0000000..044a482 --- /dev/null +++ b/tests/router_test.py @@ -0,0 +1 @@ +# todo From 54dad6eb0e05ca00e2b3e0356ff0e6359c5f4d06 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:09 +0100 Subject: [PATCH 071/451] =?UTF-8?q?=E2=9C=A8=20Init=20dspy.todo=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dspy.todo | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 dspy.todo diff --git a/dspy.todo b/dspy.todo new file mode 100644 index 0000000..5e11116 --- /dev/null +++ b/dspy.todo @@ -0,0 +1,9 @@ +To make this possible I need to: +- rewrite funcchain to be more dynamic + - modular backends + - unify syntax and schemas + - invent new syntax for special dspy modules (COT, FewShot, ...) + - seperate string and structured types logic + - + +booth are cutting edge ml libraries From fe926e70e13bdd627d3b8f4b13fbfdf8bb6b341b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:09 +0100 Subject: [PATCH 072/451] =?UTF-8?q?=E2=9C=A8=20Add=20ruff=20linter=20confi?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 00068bd..b72a11c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,3 +79,7 @@ ignore_missing_imports = true disallow_untyped_defs = true disallow_untyped_calls = true disallow_incomplete_defs = true + +[tool.ruff] +select = ["E", "F", "I"] +line-length = 120 From 078ce74cc5a29b965f60494bbd8e5341382c92ca Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:09 +0100 Subject: [PATCH 073/451] =?UTF-8?q?=F0=9F=94=A5=20Streamline=20roadmap=20t?= =?UTF-8?q?asks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmap.todo | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/roadmap.todo b/roadmap.todo index dcfbf90..f0b021b 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -1,5 +1,3 @@ -[ ] - override global settings inside chain() (4h) - [ ] - depends functionality to create nested chains and compile into runnables (10h) # add a deps thing to put into funcchain defs that takes another chain and compiles it into a runnable # so langsmith shows nested chains @@ -13,16 +11,10 @@ # So anything that is additional can be compressed to fit in the context but when other things that are important are not compressed. # Optionally you can define how to compress and where to leave the gaps (default in the middle with [...]) -[ ] - improve chain(*args interface) (2h) - -[ ] - enable union type without function calling or grammars (8h) +[ ] - enable union type without function calling (8h) [ ] - enable Error type for non union calls (4h) -[ ] - develop Matrix wrapper idea (10h) - -[ ] - easy to use Universal Router Class (20h) - [ ] - funcchain Agent Framework with Task Dependencies (30h) [ ] - convert langchain tools to funcchain agent/router (8h) @@ -43,14 +35,8 @@ [ ] - implement vision over llamacpp (8h) -[ ] - fix deepseek over llamacpp (6h) - [ ] - document examples (6h) -[ ] - split this list into priorities and optional improvements (2h) - -[ ] - check similar frameworks for new ideas - -[ ] - grammar support for enums - [ ] - split required/optional deps for only local or only openai ... + +[ ] - opt in token counting callback handler as setting to log tokens From 13a9c5ccb678aaa4e719643a73dc2809c91a3f62 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 074/451] =?UTF-8?q?=F0=9F=94=80=20Refactor=20module=20impo?= =?UTF-8?q?rts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/funcchain/__init__.py b/src/funcchain/__init__.py index 418a914..0b2df74 100644 --- a/src/funcchain/__init__.py +++ b/src/funcchain/__init__.py @@ -1,8 +1,8 @@ from pydantic import BaseModel -from .chain import achain, chain, runnable -from .settings import settings -from .types import Error +from .backend.settings import settings +from .syntax.executable import achain, chain, runnable +from .syntax.types import Error __all__ = [ "settings", From b2fd01095b85f079d736f185666ff1bf6ba0a419 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 075/451] =?UTF-8?q?=E2=9C=A8=20Add=20autotune=20init=20fil?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/autotune/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/autotune/__init__.py diff --git a/src/funcchain/autotune/__init__.py b/src/funcchain/autotune/__init__.py new file mode 100644 index 0000000..e69de29 From f72af685bbe1db53a4c4988a28fac2cefbcee738 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 076/451] =?UTF-8?q?=F0=9F=93=A6=20Add=20empty=20backend=20?= =?UTF-8?q?=5F=5Finit=5F=5F.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/backend/__init__.py diff --git a/src/funcchain/backend/__init__.py b/src/funcchain/backend/__init__.py new file mode 100644 index 0000000..e69de29 From 11229616c35586642ccc252602ebe8471da4d6f1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 077/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20compiler.py=20f?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 332 ++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 src/funcchain/backend/compiler.py diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py new file mode 100644 index 0000000..470e59a --- /dev/null +++ b/src/funcchain/backend/compiler.py @@ -0,0 +1,332 @@ +from types import UnionType +from typing import Type, TypeVar + +from langchain_core.callbacks import Callbacks +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage +from langchain_core.output_parsers import BaseOutputParser +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.runnables import RunnableSerializable +from PIL import Image +from pydantic import BaseModel + +from ..model.abilities import is_function_model, is_vision_model +from ..model.defaults import univeral_model_selector +from ..parser.grammars import pydantic_to_grammar +from ..parser.parsers import MultiToolParser, ParserBaseModel, PydanticFuncParser +from ..schema.signature import Signature +from ..syntax.meta_inspect import parser_for +from ..utils.funcs import count_tokens +from ..utils.msg_tools import msg_to_str +from ..utils.pydantic import multi_pydantic_to_functions, pydantic_to_functions +from .prompt import ( + HumanImageMessagePromptTemplate, + create_chat_prompt, + create_instruction_prompt, +) +from .settings import FuncchainSettings +from .streaming import stream_handler + +ChainOutput = TypeVar("ChainOutput") + + +# TODO: do patch instead of seperate creation +def create_union_chain( + output_type: UnionType, + instruction_prompt: HumanImageMessagePromptTemplate, + system: str, + memory: BaseChatMessageHistory, + context: list[BaseMessage], + llm: BaseChatModel, + input_kwargs: dict[str, str], +) -> RunnableSerializable[dict[str, str], BaseModel]: + """ + Compile a langchain runnable chain from the funcchain syntax. + """ + if not all(issubclass(t, BaseModel) for t in output_type.__args__): + raise RuntimeError("Funcchain union types are currently only supported for pydantic models.") + + output_types: list[Type[BaseModel]] = output_type.__args__ # type: ignore + output_type_names = [t.__name__ for t in output_types] + + input_kwargs["format_instructions"] = f"Extract to one of these output types: {output_type_names}." + + functions = multi_pydantic_to_functions(output_types) + + llm = llm.bind(**functions) # type: ignore + + prompt = create_chat_prompt( + system, + instruction_prompt, + context=[ + *context, + HumanMessage(content="Can you use a function call for the next response?"), + AIMessage(content="Yeah I can do that, just tell me what you need!"), + ], + memory=memory, + ) + + return prompt | llm | MultiToolParser(output_types=output_types) + + +# TODO: do patch instead of seperate creation +def create_pydanctic_chain( + output_type: type[BaseModel], + prompt: ChatPromptTemplate, + llm: BaseChatModel, + input_kwargs: dict[str, str], +) -> RunnableSerializable[dict[str, str], BaseModel]: + # TODO: check these format_instructions + input_kwargs["format_instructions"] = f"Extract to {output_type.__name__}." + functions = pydantic_to_functions(output_type) + + llm = llm.bind(**functions) # type: ignore + + return prompt | llm | PydanticFuncParser(pydantic_schema=output_type) + + +def create_chain( + system: str, + instruction: str, + output_type: Type[ChainOutput], + context: list[BaseMessage], + memory: BaseChatMessageHistory, + settings: FuncchainSettings, + input_kwargs: dict[str, str], +) -> RunnableSerializable[dict[str, str], ChainOutput]: + """ + Compile a langchain runnable chain from the funcchain syntax. + """ + # large language model + _llm = _gather_llm(settings) + llm = _add_custom_callbacks(_llm, settings) + + parser = parser_for(output_type, retry=settings.retry_parse, llm=llm) + + # add format instructions for parser + f_instructions = None + if parser and (settings.streaming or not is_function_model(llm)): + # streaming behavior is not supported for function models + # but for normal function models we do not need to add format instructions + instruction, f_instructions = _add_format_instructions( + parser, + instruction, + input_kwargs, + ) + + # patch inputs + _crop_large_inputs( + system, + instruction, + input_kwargs, + settings, + ) + + # for vision models + images = _handle_images(llm, memory, input_kwargs) + + # create prompts + instruction_prompt = create_instruction_prompt( + instruction, + images, + input_kwargs, + format_instructions=f_instructions, + ) + chat_prompt = create_chat_prompt(system, instruction_prompt, context, memory) + + # add formatted instruction to chat history + memory.add_message(instruction_prompt.format(**input_kwargs)) + + _inject_grammar_for_local_models(llm, output_type) + + # function model patches + if is_function_model(llm): + if isinstance(output_type, UnionType): + return create_union_chain( + output_type, + instruction_prompt, + system, + memory, + context, + llm, + input_kwargs, + ) + + if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): + if settings.streaming and hasattr(llm, "model_kwargs"): + llm.model_kwargs = {"response_format": {"type": "json_object"}} + else: + return create_pydanctic_chain( # type: ignore + output_type, + chat_prompt, + llm, + input_kwargs, + ) + assert parser is not None + return chat_prompt | llm | parser + + +def compile_chain(signature: Signature[ChainOutput]) -> RunnableSerializable[dict[str, str], ChainOutput]: + """ + Compile a signature to a runnable chain. + """ + system = ( + [msg for msg in signature.history if isinstance(msg, SystemMessage)] + or [ + SystemMessage(content=""), + ] + ).pop() + input_kwargs = {k: "" for k in signature.input_args} + + from langchain.memory import ChatMessageHistory + + memory = ChatMessageHistory(messages=signature.history) + + return create_chain( + msg_to_str(system), + signature.instruction, + signature.output_type, + signature.history, + memory, + signature.settings, + input_kwargs, + ) + + +def _add_format_instructions( + parser: BaseOutputParser, + instruction: str, + input_kwargs: dict[str, str], +) -> tuple[str, str | None]: + """ + Add parsing format instructions + to the instruction message and input_kwargs + if the output parser supports it. + """ + try: + if format_instructions := parser.get_format_instructions(): + instruction += "\n{format_instructions}" + input_kwargs["format_instructions"] = format_instructions + return instruction, format_instructions + except NotImplementedError: + return instruction, None + + +def _crop_large_inputs( + system: str, + instruction: str, + input_kwargs: dict, + settings: FuncchainSettings, +) -> None: + """ + Crop large inputs to avoid exceeding the maximum number of tokens. + """ + base_tokens = count_tokens(instruction + system) + for k, v in input_kwargs.copy().items(): + if isinstance(v, str): + content_tokens = count_tokens(v) + if base_tokens + content_tokens > settings.context_lenght: + input_kwargs[k] = v[: (settings.context_lenght - base_tokens) * 2 // 3] + print("Truncated: ", len(input_kwargs[k])) + + +def _handle_images( + llm: BaseChatModel, + memory: BaseChatMessageHistory, + input_kwargs: dict[str, str], +) -> list[Image.Image]: + """ + Handle images for vision models. + """ + images = [v for v in input_kwargs.values() if isinstance(v, Image.Image)] + if is_vision_model(llm): + for k in list(input_kwargs.keys()): + if isinstance(input_kwargs[k], Image.Image): + del input_kwargs[k] + elif images: + raise RuntimeError("Images as input are only supported for vision models.") + elif _history_contains_images(memory): + print("Warning: Images in chat history are ignored for non-vision models.") + memory.messages = _clear_images_from_history(memory.messages) + + return images + + +def _inject_grammar_for_local_models(llm: BaseChatModel, output_type: type) -> None: + """ + Inject GBNF grammar into local models. + """ + try: + from funcchain.utils._llms import ChatOllama + except: # noqa + pass + else: + if isinstance(llm, ChatOllama): + if isinstance(output_type, UnionType): + raise NotImplementedError("Union types are not yet supported for LlamaCpp models.") # TODO: implement + + if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): + llm.grammar = pydantic_to_grammar(output_type) + if issubclass(output_type, ParserBaseModel): + llm.grammar = output_type.custom_grammar() + + +def _gather_llm(settings: FuncchainSettings) -> BaseChatModel: + if isinstance(settings.llm, BaseChatModel): + llm = settings.llm + else: + llm = univeral_model_selector(settings) + + if not llm: + raise RuntimeError( + "No language model provided. Either set the llm environment variable or " + "pass a model to the `chain` function." + ) + return llm + + +def _add_custom_callbacks(llm: BaseChatModel, settings: FuncchainSettings) -> BaseChatModel: + callbacks: Callbacks = [] + + if handler := stream_handler.get(): + callbacks = [handler] + + if settings.console_stream: + from .streaming import AsyncStreamHandler + + callbacks = [ + AsyncStreamHandler(print, {"end": "", "flush": True}), + ] + + if callbacks: + settings.streaming = True + if hasattr(llm, "streaming"): + llm.streaming = True + llm.callbacks = callbacks + + return llm + + +def _history_contains_images(history: BaseChatMessageHistory) -> bool: + """ + Check if the chat history contains images. + """ + for message in history.messages: + if isinstance(message.content, list): + for content in message.content: + if isinstance(content, dict) and content.get("type") == "image_url": + return True + return False + + +def _clear_images_from_history(history: list[BaseMessage]) -> list[BaseMessage]: + """ + Remove images from the chat history. + """ + for message in history: + if isinstance(message.content, list): + for content in message.content: + if isinstance(content, dict) and content.get("type") == "image_url": + message.content.remove(content) + return history From 3798e84da1c6a160995a8077f26cdfa27a4c0c70 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 078/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20prompt.py=20fil?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/prompt.py | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/funcchain/backend/prompt.py diff --git a/src/funcchain/backend/prompt.py b/src/funcchain/backend/prompt.py new file mode 100644 index 0000000..148b8df --- /dev/null +++ b/src/funcchain/backend/prompt.py @@ -0,0 +1,168 @@ +from string import Formatter +from typing import Any, Optional, Type + +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.prompts.chat import ( + BaseStringMessagePromptTemplate, + MessagePromptTemplateT, +) +from langchain_core.prompts.prompt import PromptTemplate +from PIL import Image +from pydantic import BaseModel + +from ..utils.image import image_to_base64_url + + +def create_instruction_prompt( + instruction: str, + images: list[Image.Image], + input_kwargs: dict[str, Any], + format_instructions: Optional[str] = None, +) -> "HumanImageMessagePromptTemplate": + template_format = _determine_format(instruction) + + required_f_str_vars = _extract_fstring_vars(instruction) + + _filter_fstring_vars(input_kwargs) + + inject_vars = [f"{var.upper()}:\n{{{var}}}\n" for var, _ in input_kwargs.items() if var not in required_f_str_vars] + added_instruction = "\n".join(inject_vars) + instruction = added_instruction + instruction + + images = [image_to_base64_url(image) for image in images] + + return HumanImageMessagePromptTemplate.from_template( + template=instruction, + template_format=template_format, + images=images, + partial_variables={"format_instructions": format_instructions} if format_instructions else None, + ) + + +def create_chat_prompt( + system: str, + instruction_template: "HumanImageMessagePromptTemplate", + context: list[BaseMessage], + memory: BaseChatMessageHistory, +) -> ChatPromptTemplate: + """ + Compose a chat prompt from a system message, + context and instruction template. + """ + # remove leading system message in case to not have two + if system and memory.messages and isinstance(memory.messages[0], SystemMessage): + memory.messages.pop(0) + + if memory.messages and isinstance(memory.messages[-1], HumanMessage): + return ChatPromptTemplate.from_messages( + [ + *([SystemMessage(content=system)] if system else []), + *memory.messages, + *context, + ] + ) + + return ChatPromptTemplate.from_messages( + [ + *([SystemMessage(content=system)] if system else []), + *memory.messages, + *context, + instruction_template, + ] + ) + + +def _determine_format( + instruction: str, +) -> str: + return "jinja2" if "{{" in instruction or "{%" in instruction else "f-string" + + +def _extract_fstring_vars(template: str) -> list[str]: + """ + TODO: enable jinja2 check + Function to extract f-string variables from a string. + """ + return [ + field_name # print("field_name:", field_name) or field_name.split(".")[0] + for _, field_name, _, _ in Formatter().parse(template) + if field_name is not None + ] + + +def _filter_fstring_vars( + input_kwargs: dict[str, Any], +) -> None: + """Mutate input_kwargs to filter out non-string values.""" + keys_to_remove = [ + key + for key, value in input_kwargs.items() + if not (isinstance(value, str) or isinstance(value, BaseModel)) # TODO: remove BaseModel + ] + for key in keys_to_remove: + del input_kwargs[key] + + +class HumanImageMessagePromptTemplate(BaseStringMessagePromptTemplate): + """Human message prompt template. This is a message sent from the user.""" + + images: list[str] = [] + + def format(self, **kwargs: Any) -> BaseMessage: + """Format the prompt template. + + Args: + **kwargs: Keyword arguments to use for formatting. + + Returns: + Formatted message. + """ + text = self.prompt.format(**kwargs) + return HumanMessage( + content=[ + { + "type": "text", + "text": text, + }, + *[ + { + "type": "image_url", + "image_url": { + "url": image, + "detail": "auto", + }, + } + for image in self.images + ], + ], + additional_kwargs=self.additional_kwargs, + ) + + @classmethod + def from_template( + cls: Type[MessagePromptTemplateT], + template: str, + template_format: str = "f-string", + partial_variables: Optional[dict[str, Any]] = None, + images: list[str] = [], + **kwargs: Any, + ) -> MessagePromptTemplateT: + """Create a class from a string template. + + Args: + template: a template. + template_format: format of the template. + **kwargs: keyword arguments to pass to the constructor. + + Returns: + A new instance of this class. + """ + prompt = PromptTemplate.from_template( + template, + template_format=template_format, + partial_variables=partial_variables, + ) + kwargs["images"] = images + return cls(prompt=prompt, **kwargs) From d9911074d054049bdfba8711c945d5aba06da54a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 079/451] =?UTF-8?q?=F0=9F=94=A7=20Renamed=20settings.py=20?= =?UTF-8?q?file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/settings.py | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/funcchain/backend/settings.py diff --git a/src/funcchain/backend/settings.py b/src/funcchain/backend/settings.py new file mode 100644 index 0000000..1b6e496 --- /dev/null +++ b/src/funcchain/backend/settings.py @@ -0,0 +1,94 @@ +""" +Funcchain Settings: +Automatically loads environment variables from .env file +""" +from typing import Optional + +from langchain_core.language_models import BaseChatModel +from pydantic import Field +from pydantic_settings import BaseSettings +from typing_extensions import TypedDict + + +class FuncchainSettings(BaseSettings): + debug: bool = True + + llm: BaseChatModel | str = Field( + default="openai/gpt-3.5-turbo-1106", + validate_default=False, + ) + + console_stream: bool = False + + default_system_prompt: str = "" + + retry_parse: int = 3 + retry_parse_sleep: float = 0.1 + + # KEYS + openai_api_key: Optional[str] = None + azure_api_key: Optional[str] = None + anthropic_api_key: Optional[str] = None + google_api_key: Optional[str] = None + + # MODEL KWARGS + verbose: bool = False + streaming: bool = False + max_tokens: int = 2048 + temperature: float = 0.1 + + # OLLAMA KWARGS + context_lenght: int = 8196 + n_gpu_layers: int = 50 + keep_loaded: bool = False + + def model_kwargs(self) -> dict: + return { + "verbose": self.verbose, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + "streaming": self.streaming, + } + + def openai_kwargs(self) -> dict: + return { + "openai_api_key": self.openai_api_key, + } + + def ollama_kwargs(self) -> dict: + return { + "n_ctx": self.context_lenght, + "use_mlock": self.keep_loaded, + "n_gpu_layers": self.n_gpu_layers, + } + + +settings = FuncchainSettings() + + +class SettingsOverride(TypedDict, total=False): + llm: BaseChatModel | str + + verbose: bool + temperature: float + max_tokens: int + streaming: bool + retry_parse: int + context_lenght: int + + +def create_local_settings(override: Optional[SettingsOverride] = None) -> FuncchainSettings: + if override: + if override["llm"] is None: + override["llm"] = settings.llm + return settings.model_copy(update=dict(override)) + return settings + + +# load langsmith logging vars +try: + import dotenv +except ImportError: + pass +else: + dotenv.load_dotenv() From 44e64942c96ecd368a60a42ef86c1946d53db34d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 080/451] =?UTF-8?q?=F0=9F=94=80=20Renamed=20streaming.py?= =?UTF-8?q?=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/streaming.py | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/funcchain/backend/streaming.py diff --git a/src/funcchain/backend/streaming.py b/src/funcchain/backend/streaming.py new file mode 100644 index 0000000..494b0f5 --- /dev/null +++ b/src/funcchain/backend/streaming.py @@ -0,0 +1,111 @@ +from contextlib import asynccontextmanager, contextmanager +from contextvars import ContextVar +from typing import Any, AsyncGenerator, Awaitable, Callable, Coroutine, Generator +from uuid import UUID + +from langchain_core.callbacks.base import AsyncCallbackHandler +from langchain_core.messages import BaseMessage +from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult + + +class AsyncStreamHandler(AsyncCallbackHandler): + """Async callback handler that can be used to handle callbacks from langchain_core.""" + + def __init__(self, fn: Callable[[str], Awaitable[None] | None], default_kwargs: dict) -> None: + self.fn = fn + self.default_kwargs = default_kwargs + self.cost: float = 0.0 + self.tokens: int = 0 + + async def on_chat_model_start( + self, + serialized: dict[str, Any], + messages: list[list[BaseMessage]], + *, + run_id: UUID, + parent_run_id: UUID | None = None, + tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + **kwargs: Any, + ) -> Any: + # from .utils import count_tokens + # for lists in messages: + # for message in lists: + # if message.content: + # if isinstance(message.content, str): + # self.tokens += count_tokens(message.content) + # elif isinstance(message.content, list): + # print("token_counting", message.content) + # # self.tokens += count_tokens(message) + pass + + async def on_llm_new_token( + self, + token: str, + *, + chunk: GenerationChunk | ChatGenerationChunk | None = None, + run_id: UUID, + parent_run_id: UUID | None = None, + tags: list[str] | None = None, + **kwargs: Any, + ) -> None: + if isinstance(self.fn, Coroutine): + await self.fn(token, **self.default_kwargs) + else: + self.fn(token, **self.default_kwargs) + + async def on_llm_end( + self, + response: LLMResult, + *, + run_id: UUID, + parent_run_id: UUID | None = None, + tags: list[str] | None = None, + **kwargs: Any, + ) -> None: + if self.fn is print: + print("\n") + + +stream_handler: ContextVar[AsyncStreamHandler | None] = ContextVar("stream_handler", default=None) + + +@contextmanager +def stream_to(fn: Callable[[str], None], **kwargs: Any) -> Generator[AsyncStreamHandler, None, None]: + """ + Stream the llm tokens to a given function. + + Example: + >>> with stream_to(print): + ... # your chain calls here + """ + import builtins + + import rich + + if (fn is builtins.print or fn is rich.print) and kwargs == {}: + kwargs = {"end": "", "flush": True} + + cb = AsyncStreamHandler(fn, kwargs) + stream_handler.set(cb) + yield cb + stream_handler.set(None) + + +@asynccontextmanager +async def astream_to( + fn: Callable[[str], Awaitable[None] | None], **kwargs: Any +) -> AsyncGenerator[AsyncStreamHandler, None]: + """ + Asyncronously stream the llm tokens to a given function. + + Example: + >>> async with astream_to(print): + ... # your chain calls here + """ + if fn is print and kwargs == {}: + kwargs = {"end": "", "flush": True} + cb = AsyncStreamHandler(fn, kwargs) + stream_handler.set(cb) + yield cb + stream_handler.set(None) From 3fd20df3525025c27b5cfb50b658b741bb97a59e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 081/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20init=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/__init__.py | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/funcchain/chain/__init__.py diff --git a/src/funcchain/chain/__init__.py b/src/funcchain/chain/__init__.py deleted file mode 100644 index bf3e470..0000000 --- a/src/funcchain/chain/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .interface import achain, chain -from .runnables import runnable - -__all__ = ["chain", "achain", "runnable"] From e9a536e7bd030fc64b36084ef318b1f2e72816ff Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 082/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20chain?= =?UTF-8?q?/interface.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/interface.py | 52 -------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/funcchain/chain/interface.py diff --git a/src/funcchain/chain/interface.py b/src/funcchain/chain/interface.py deleted file mode 100644 index 82d73f8..0000000 --- a/src/funcchain/chain/interface.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import TypeVar - -from langchain.memory import ChatMessageHistory -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.messages import BaseMessage - -from ..settings import SettingsOverride, get_settings -from .invoke import ainvoke, invoke - -ChainOutput = TypeVar("ChainOutput") - - -def chain( - system: str | None = None, - instruction: str | None = None, - context: list[BaseMessage] = [], - memory: BaseChatMessageHistory | None = None, - settings_override: SettingsOverride | None = None, - **input_kwargs: str, -) -> ChainOutput: # type: ignore - """ - Generate response of llm for provided instructions. - """ - return invoke( - system, - instruction, - context, - memory or ChatMessageHistory(), - get_settings(settings_override), - input_kwargs, - ) - - -async def achain( - system: str | None = None, - instruction: str | None = None, - context: list[BaseMessage] = [], - memory: BaseChatMessageHistory | None = None, - settings_override: SettingsOverride | None = None, - **input_kwargs: str, -) -> ChainOutput: - """ - Asyncronously generate response of llm for provided instructions. - """ - return await ainvoke( - system, - instruction, - context, - memory or ChatMessageHistory(), - get_settings(settings_override), - input_kwargs, - ) From 2edab5bd5f75d6526b012fecd6d29b935961be0e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 083/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20invok?= =?UTF-8?q?e.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/invoke.py | 93 ----------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 src/funcchain/chain/invoke.py diff --git a/src/funcchain/chain/invoke.py b/src/funcchain/chain/invoke.py deleted file mode 100644 index 5c783b8..0000000 --- a/src/funcchain/chain/invoke.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Any, TypeVar - -from langchain_core.callbacks.base import Callbacks -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.messages import BaseMessage -from langchain_core.runnables import RunnableSerializable - -from ..settings import FuncchainSettings -from ..utils import ( - from_docstring, - get_output_type, - get_parent_frame, - kwargs_from_parent, - log_openai_callback, - retry_parse, -) -from .creation import create_chain - -T = TypeVar("T") - - -@retry_parse -@log_openai_callback -def invoke( - system: str | None, - instruction: str | None, - context: list[BaseMessage], - memory: BaseChatMessageHistory, - settings: FuncchainSettings, - input_kw: dict[str, str] = {}, - callbacks: Callbacks = None, -) -> Any: # type: ignore - # default values - output_type = get_output_type() - input_kw.update(kwargs_from_parent()) - system = system or settings.default_system_prompt - instruction = instruction or from_docstring() - - chain: RunnableSerializable[dict[str, str], Any] = create_chain( - system, - instruction, - output_type, - context, - memory, - settings, - input_kw, - ) - result = chain.invoke( - input_kw, {"run_name": get_parent_frame(5).function, "callbacks": callbacks} - ) - - if isinstance(result, str): - # TODO: function calls? - memory.add_ai_message(result) - - return result - - -@retry_parse -@log_openai_callback -async def ainvoke( - system: str | None, - instruction: str | None, - context: list[BaseMessage], - memory: BaseChatMessageHistory, - settings: FuncchainSettings, - input_kw: dict[str, str] = {}, - callbacks: Callbacks = None, -) -> Any: - # default values - output_type = get_output_type() - input_kw.update(kwargs_from_parent()) - system = system or settings.default_system_prompt - instruction = instruction or from_docstring() - - chain: RunnableSerializable[dict[str, str], Any] = create_chain( - system, - instruction, - output_type, - context, - memory, - settings, - input_kw, - ) - result = await chain.ainvoke( - input_kw, {"run_name": get_parent_frame(5).function, "callbacks": callbacks} - ) - - if isinstance(result, str): - # TODO: function calls? - memory.add_ai_message(result) - - return result From c8c1e4b0df8871345c32be6e6e0543a7683c15ad Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 084/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20obsol?= =?UTF-8?q?ete=20runnables.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/runnables.py | 38 -------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/funcchain/chain/runnables.py diff --git a/src/funcchain/chain/runnables.py b/src/funcchain/chain/runnables.py deleted file mode 100644 index 55f52ad..0000000 --- a/src/funcchain/chain/runnables.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import TypeVar, Type -from langchain_core.runnables import RunnableSerializable -from langchain.memory import ChatMessageHistory -from .creation import create_chain -from ..settings import SettingsOverride, get_settings - -T = TypeVar("T") - - -def runnable( - instruction: str, - output_type: Type[T], - input_args: list[str] = [], - context: list = [], - system: str = "", - settings_override: SettingsOverride | None = None, -) -> RunnableSerializable[dict[str, str], T]: - """ - Experimental replacement for using the funcchain syntax. - """ - instruction = "\n" + instruction - chain: RunnableSerializable[dict[str, str], T] = create_chain( - system=system, - instruction=instruction, - output_type=output_type, - context=context, - memory=ChatMessageHistory(), - settings=get_settings(settings_override), - input_kwargs={k: "" for k in input_args}, - ) - - # TODO: rewrite without original chain creation - # gather llm - # evaluate model capabilities - # get - # create prompt template - - return chain From 7642d7986f553570b0da64948008b42cb1af0b88 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 085/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20compo?= =?UTF-8?q?nents.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/components.py | 191 ------------------------------------ 1 file changed, 191 deletions(-) delete mode 100644 src/funcchain/components.py diff --git a/src/funcchain/components.py b/src/funcchain/components.py deleted file mode 100644 index 26c3652..0000000 --- a/src/funcchain/components.py +++ /dev/null @@ -1,191 +0,0 @@ -from enum import Enum -from typing import AsyncIterator, Callable, Any, Iterator, TypeVar, Optional -from typing_extensions import TypedDict -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.language_models import BaseChatModel -from langchain_core.messages import AIMessage, HumanMessage -from langchain_core.runnables import ( - Runnable, - RunnableSerializable, - RouterRunnable, - RunnableLambda, -) -from langchain_core.runnables.config import RunnableConfig -from langchain_core.runnables.router import RouterInput -from funcchain import runnable -from funcchain.utils.msg_tools import msg_to_str - - -class Route(TypedDict): - handler: Callable | Runnable - description: str - - -Routes = dict[str, Route] - -ResponseType = TypeVar("ResponseType") - - -class ChatRouter(RouterRunnable[ResponseType]): - """A router component that can be used to route user requests to different handlers.""" - - def __init__( - self, - *, - routes: Routes, - history: Optional[BaseChatMessageHistory] = None, - llm: Optional[BaseChatModel | str] = None, - add_default: bool = False, - **kwargs: Any, - ) -> None: - super().__init__( - runnables={name: run["handler"] for name, run in routes.items()}, - **kwargs, - ) - self.llm = llm - self.history = history - self.routes = self._add_default_handler(routes) if add_default else routes - - class Config: - arbitrary_types_allowed = True - extra = "allow" - - @classmethod - def create_router( - cls, - *, - routes: Routes, - history: Optional[BaseChatMessageHistory] = None, - llm: Optional[BaseChatModel | str] = None, - **kwargs: Any, - ) -> RunnableSerializable[Any, ResponseType]: - router = cls( - routes=routes, llm=llm, history=history, add_default=True, **kwargs - ) - return { - "input": lambda x: x, - "key": ( - lambda x: { - # "image": x.images[0], - # "user_request": x.__str__(), - "user_request": x.content, - "routes": router._routes_repr(), - } - ) - | cls._selector(routes, llm, history) - | RunnableLambda(lambda x: x.selector.__str__()), - } | router - - @staticmethod - def _selector( - routes: Routes, - llm: BaseChatModel | str | None = None, - history: BaseChatMessageHistory | None = None, - ) -> Runnable[dict[str, str], Any]: - RouteChoices = Enum( # type: ignore - "RouteChoices", - {r: r for r in routes.keys()}, - type=str, - ) - from pydantic import BaseModel, Field - - class RouterModel(BaseModel): - selector: RouteChoices = Field( - default="default", - description="Enum of the available routes.", - ) - - return runnable( - instruction="Given the user request select the appropriate route.", - input_args=["user_request", "routes"], # todo: optional images - output_type=RouterModel, - context=history.messages if history else [], - settings_override={"llm": llm}, - ) - - def _add_default_handler(self, routes: Routes) -> Routes: - if "default" not in routes.keys(): - routes["default"] = { - "handler": ( - RunnableLambda(lambda x: msg_to_str(x)) - | runnable( - instruction="{user_request}", - input_args=["user_request"], - output_type=str, - settings_override={"llm": self.llm}, - ) - | RunnableLambda(lambda x: AIMessage(content=x)) - ), - "description": ( - "Choose this for everything else like " - "normal questions or random things.\n" - "As example: 'How does this work?' or " - "'Whatsup' or 'What is the meaning of life?'" - ), - } - return routes - - def _routes_repr(self) -> str: - return "\n".join( - [ - f"{route_name}: {route['description']}" - for route_name, route in self.routes.items() - ] - ) - - def post_update_history(self, input: RouterInput, output: ResponseType) -> None: - input = input["input"] - if self.history: - if isinstance(input, HumanMessage): - self.history.add_message(input) - if isinstance(output, AIMessage): - self.history.add_message(output) - - # TODO: deprecate - def invoke_route(self, user_query: str, /, **kwargs: Any) -> ResponseType: - """Deprecated. Use invoke instead.""" - route_query = self._selector(self.routes) - - selected_route = route_query.invoke( - input={"user_request": user_query, "routes": self._routes_repr()} - ).selector - return self.routes[selected_route]["handler"](user_query, **kwargs) # type: ignore - - def invoke( - self, input: RouterInput, config: RunnableConfig | None = None - ) -> ResponseType: - output = super().invoke(input, config) - self.post_update_history(input, output) - return output - - async def ainvoke( - self, - input: RouterInput, - config: RunnableConfig | None = None, - **kwargs: Any | None, - ) -> ResponseType: - output = await super().ainvoke(input, config, **kwargs) - self.post_update_history(input, output) - return output - - def stream( - self, - input: RouterInput, - config: RunnableConfig | None = None, - **kwargs: Any | None, - ) -> Iterator[ResponseType]: - for i in super().stream(input, config, **kwargs): - yield (last := i) - if last: - self.post_update_history(input, last) - - async def astream( - self, - input: RouterInput, - config: RunnableConfig | None = None, - **kwargs: Any | None, - ) -> AsyncIterator[ResponseType]: - async for ai in super().astream(input, config, **kwargs): - yield (last := ai) - if last: - self.post_update_history(input, last) From 623043443b06c24b760d8dd091ba290d3c66b7cc Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 086/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20exceptions.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/exceptions.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/funcchain/exceptions.py diff --git a/src/funcchain/exceptions.py b/src/funcchain/exceptions.py deleted file mode 100644 index c463839..0000000 --- a/src/funcchain/exceptions.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any - -from langchain_core.exceptions import OutputParserException -from langchain_core.messages import BaseMessage - - -class ParsingRetryException(OutputParserException): - """Exception raised when parsing fails.""" - - def __init__(self, *args: Any, message: BaseMessage, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.message = message From 08f667d5d1c4dfbe55b200ae7bd22861a9b81070 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 087/451] =?UTF-8?q?=F0=9F=93=A6=20Add=20model=20package=20?= =?UTF-8?q?init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/funcchain/model/__init__.py diff --git a/src/funcchain/model/__init__.py b/src/funcchain/model/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/funcchain/model/__init__.py @@ -0,0 +1 @@ + From fb0d04a8e333a3627e2d16e9a2ae3fcb8d706a7d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 088/451] =?UTF-8?q?=F0=9F=94=8D=20Add=20model=20type=20che?= =?UTF-8?q?cks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/abilities.py | 78 ++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/funcchain/model/abilities.py diff --git a/src/funcchain/model/abilities.py b/src/funcchain/model/abilities.py new file mode 100644 index 0000000..70ec683 --- /dev/null +++ b/src/funcchain/model/abilities.py @@ -0,0 +1,78 @@ +from langchain.chat_models import ChatOllama, ChatOpenAI +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import HumanMessage, SystemMessage + +verified_openai_function_models = [ + "gpt-4", + "gpt-4-0613", + "gpt-4-1106-preview", + "gpt-4-32k", + "gpt-4-32k-0613", + "gpt-3.5-turbo", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-1106", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-16k-0613", +] + +verified_openai_vision_models = [ + "gpt-4-vision-preview", +] + +verified_ollama_vision_models = [ + "llava", + "bakllava", +] + + +def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: + if not isinstance(llm, BaseChatModel): + return "base_model" + if isinstance(llm, ChatOpenAI): + if llm.model_name in verified_openai_vision_models: + return "vision_model" + if llm.model_name in verified_openai_function_models: + return "function_model" + try: + if func_check: + llm.predict_messages( + [ + SystemMessage(content=("This is a test message to see " "if the model can run functions.")), + HumanMessage(content="Hello!"), + ], + functions=[ + { + "name": "print", + "description": "show the input", + "parameters": { + "properties": { + "__arg1": {"title": "__arg1", "type": "string"}, + }, + "required": ["__arg1"], + "type": "object", + }, + } + ], + ) + except Exception: + return "chat_model" + else: + return "function_model" + elif isinstance(llm, ChatOllama): + for model in verified_ollama_vision_models: + if llm.model in model: + return "vision_model" + + return "chat_model" + + +def is_function_model( + llm: BaseChatModel, +) -> bool: + return gather_llm_type(llm) == "function_model" + + +def is_vision_model( + llm: BaseChatModel, +) -> bool: + return gather_llm_type(llm) == "vision_model" From 433fb5c04e7514eaa703c2e0d94f240048a9903c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 089/451] =?UTF-8?q?=F0=9F=94=81=20Rename=20defaults.py=20f?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/defaults.py | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/funcchain/model/defaults.py diff --git a/src/funcchain/model/defaults.py b/src/funcchain/model/defaults.py new file mode 100644 index 0000000..de9f585 --- /dev/null +++ b/src/funcchain/model/defaults.py @@ -0,0 +1,107 @@ +from typing import Any + +from langchain_core.language_models import BaseChatModel + +from ..backend.settings import FuncchainSettings + + +def univeral_model_selector( + settings: FuncchainSettings, + **model_kwargs: Any, +) -> BaseChatModel: + """ + Automatically selects the best possible model for a given ModelName. + You can use this schema: + + "provider/model_name:" + + and automatically select the right model for you. + You can add optional model kwargs like temperature. + + Examples: + - "openai/gpt-3.5-turbo" + - "anthropic/claude-2" + - "ollama/deepseek-llm-7b-chat" + + Supported: + [ openai, anthropic, google, ollama ] + + Raises: + - ModelNotFoundError, when the model is not found. + """ + model_name = settings.llm if isinstance(settings.llm, str) else "" + model_kwargs.update(settings.model_kwargs()) + + if model_name: + mtype, name = model_name.split("/") if "/" in model_name else ("", model_name) + mtype = mtype.lower() + + model_kwargs["model_name"] = name + + try: + match mtype: + case "openai": + from langchain.chat_models import ChatOpenAI + + model_kwargs.update(settings.openai_kwargs()) + return ChatOpenAI(**model_kwargs) + + case "anthropic": + from langchain.chat_models import ChatAnthropic + + return ChatAnthropic(**model_kwargs) + + case "google": + from langchain.chat_models import ChatGooglePalm + + return ChatGooglePalm(**model_kwargs) + + case "ollama": + from ..utils._llms import ChatOllama + + model = model_kwargs.pop("model_name") + model_kwargs.update(settings.ollama_kwargs()) + return ChatOllama(model=model, **model_kwargs) + + except Exception as e: + print("ERROR:", e) + raise e + + try: + if "gpt-4" in name or "gpt-3.5" in name: + from langchain.chat_models import ChatOpenAI + + model_kwargs.update(settings.openai_kwargs()) + return ChatOpenAI(**model_kwargs) + + except Exception as e: + print(e) + + model_kwargs.pop("model_name") + + if settings.openai_api_key: + from langchain.chat_models import ChatOpenAI + + model_kwargs.update(settings.openai_kwargs()) + return ChatOpenAI(**model_kwargs) + + if settings.azure_api_key: + from langchain.chat_models import AzureChatOpenAI + + return AzureChatOpenAI(**model_kwargs) + + if settings.anthropic_api_key: + from langchain.chat_models import ChatAnthropic + + return ChatAnthropic(**model_kwargs) + + if settings.google_api_key: + from langchain.chat_models import ChatGooglePalm + + return ChatGooglePalm(**model_kwargs) + + raise ValueError( + "Could not read llm selector string. Please check " + "[here](https://github.com/shroominic/funcchain/blob/main/MODELS.md) " + "for more info." + ) From 57f667067bc50399a41fcb656ccd5c5758fea037 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 090/451] =?UTF-8?q?=E2=9C=A8=20Add=20empty=20parser=20modu?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/parser/__init__.py diff --git a/src/funcchain/parser/__init__.py b/src/funcchain/parser/__init__.py new file mode 100644 index 0000000..e69de29 From db134a345bba0aecfbb1ade6782bcc30101f46a8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 091/451] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20parser=20mod?= =?UTF-8?q?ule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/custom.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/parser/custom.py diff --git a/src/funcchain/parser/custom.py b/src/funcchain/parser/custom.py new file mode 100644 index 0000000..e69de29 From bbd1ea296d01418ad8facd927f1a52f695feef84 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 092/451] =?UTF-8?q?=F0=9F=94=80=20Renamed=20grammars.py=20?= =?UTF-8?q?file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/grammars.py | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/funcchain/parser/grammars.py diff --git a/src/funcchain/parser/grammars.py b/src/funcchain/parser/grammars.py new file mode 100644 index 0000000..47d785d --- /dev/null +++ b/src/funcchain/parser/grammars.py @@ -0,0 +1,130 @@ +import json +import re +from typing import Type + +from pydantic import BaseModel + +SPACE_RULE = '" "?' + +PRIMITIVE_RULES = { + "boolean": '("true" | "false") space', + "number": '("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space', + "integer": '("-"? ([0-9] | [1-9] [0-9]*)) space', + "string": r""" "\"" ( + [^"\\] | + "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) + )* "\"" space """, + "null": '"null" space', +} + +INVALID_RULE_CHARS_RE = re.compile(r"[^a-zA-Z0-9-]+") +GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"]') +GRAMMAR_LITERAL_ESCAPES = {"\r": "\\r", "\n": "\\n", '"': '\\"'} + + +class SchemaConverter: + def __init__(self, prop_order: dict, defs: dict) -> None: + self._prop_order = prop_order + self._defs = defs + self._rules = {"space": SPACE_RULE} + + def _format_literal(self, literal: str) -> str: + escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub( + lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)), # type: ignore + json.dumps(literal), + ) + return f'"{escaped}"' + + def _add_rule(self, name: str, rule: str) -> str: + esc_name = INVALID_RULE_CHARS_RE.sub("-", name) + if esc_name not in self._rules or self._rules[esc_name] == rule: + key = esc_name + else: + i = 0 + while f"{esc_name}{i}" in self._rules: + i += 1 + key = f"{esc_name}{i}" + self._rules[key] = rule + return key + + def visit(self, schema: dict, name: str) -> str: + schema_type = schema.get("type") + rule_name = name or "root" + + if "$ref" in schema: + ref_name = schema["$ref"].split("/")[-1] + assert ref_name in self._defs, f"Unresolved reference: {schema['$ref']}" + return self.visit(self._defs[ref_name], ref_name) + + elif "oneOf" in schema or "anyOf" in schema: + rule = " | ".join( + ( + self.visit(alt_schema, f'{name}{"-" if name else ""}{i}') + for i, alt_schema in enumerate(schema.get("oneOf") or schema["anyOf"]) + ) + ) + return self._add_rule(rule_name, rule) + + elif "allOf" in schema: + rule = " ".join( + ( + self.visit(sub_schema, f'{name}{"-" if name else ""}{i}') + for i, sub_schema in enumerate(schema["allOf"]) + ) + ) + return self._add_rule(rule_name, rule) + + elif "const" in schema: + return self._add_rule(rule_name, self._format_literal(schema["const"])) + + elif "enum" in schema: + rule = " | ".join((self._format_literal(v) for v in schema["enum"])) + return self._add_rule(rule_name, rule) + + elif schema_type == "object" and "properties" in schema: + # TODO: `required` keyword + prop_order = self._prop_order + prop_pairs = sorted( + schema["properties"].items(), + # sort by position in prop_order (if specified) then by key + key=lambda kv: (prop_order.get(kv[0], len(prop_order)), kv[0]), + ) + + rule = '"{" space' + for i, (prop_name, prop_schema) in enumerate(prop_pairs): + prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}') + if i > 0: + rule += ' "," space' + rule += rf' {self._format_literal(prop_name)} space ":" space {prop_rule_name}' + rule += ' "}" space' + + return self._add_rule(rule_name, rule) + + elif schema_type == "array" and "items" in schema: + # TODO `prefixItems` keyword + item_rule_name = self.visit(schema["items"], f'{name}{"-" if name else ""}item') + rule = f'"[" space ({item_rule_name} ("," space {item_rule_name})*)? "]" space' + return self._add_rule(rule_name, rule) + + else: + assert schema_type in PRIMITIVE_RULES, f"Unrecognized schema: {schema}" + return self._add_rule( + "root" if rule_name == "root" else schema_type, + PRIMITIVE_RULES[schema_type], + ) + + def format_grammar(self) -> str: + return "\n".join((f"{name} ::= {rule}" for name, rule in self._rules.items())) + + +def schema_to_grammar(json_schema: dict) -> str: + schema = json_schema + prop_order = {name: idx for idx, name in enumerate(schema["properties"].keys())} + defs = schema.get("$defs", {}) + converter = SchemaConverter(prop_order, defs) + converter.visit(schema, "") + return converter.format_grammar() + + +def pydantic_to_grammar(model: Type[BaseModel]) -> str: + return schema_to_grammar(model.model_json_schema()) From 39e8b0a52b28dfd5a8c2b63f14e1c78b28623466 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 093/451] =?UTF-8?q?=F0=9F=94=80=20Renamed=20parsers.py=20f?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/parsers.py | 252 ++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/funcchain/parser/parsers.py diff --git a/src/funcchain/parser/parsers.py b/src/funcchain/parser/parsers.py new file mode 100644 index 0000000..fc20d42 --- /dev/null +++ b/src/funcchain/parser/parsers.py @@ -0,0 +1,252 @@ +import copy +import json +import re +from typing import Callable, Optional, Type, TypeVar + +import yaml # type: ignore +from langchain_core.exceptions import OutputParserException +from langchain_core.language_models import BaseChatModel +from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser +from langchain_core.outputs import ChatGeneration, Generation +from langchain_core.runnables import Runnable +from pydantic import BaseModel, ValidationError + +from ..syntax.types import CodeBlock as CodeBlock +from ..syntax.types import ParserBaseModel +from ..utils.msg_tools import msg_to_str + +T = TypeVar("T") + + +class LambdaOutputParser(BaseOutputParser[T]): + _parse: Optional[Callable[[str], T]] = None + + def parse(self, text: str) -> T: + if self._parse is None: + raise NotImplementedError("LambdaOutputParser.lambda_parse() is not implemented") + return self._parse(text) + + @property + def _type(self) -> str: + return "lambda" + + +class BoolOutputParser(BaseOutputParser[bool]): + def parse(self, text: str) -> bool: + return text.strip()[:1].lower() == "y" + + def get_format_instructions(self) -> str: + return "\nAnswer only with 'Yes' or 'No'." + + @property + def _type(self) -> str: + return "bool" + + +M = TypeVar("M", bound=BaseModel) + + +class PydanticFuncParser(BaseGenerationOutputParser[M]): + pydantic_schema: Type[M] + args_only: bool = False + + def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException( + "This output parser can only be used with a chat generation.", + ) + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + + if self.args_only: + _result = func_call["arguments"] + else: + _result = func_call + try: + if self.args_only: + pydantic_args = self.pydantic_schema.model_validate_json(_result) + else: + pydantic_args = self.pydantic_schema.model_validate_json(_result["arguments"]) + except ValidationError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + return pydantic_args + + +class MultiToolParser(BaseGenerationOutputParser[M]): + output_types: list[Type[M]] + args_only: bool = False + + def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: + function_call = self._pre_parse_function_call(result) + + output_type_names = [t.__name__.lower() for t in self.output_types] + + if function_call["name"] not in output_type_names: + raise OutputParserException("Invalid function call") + + print(function_call["name"]) + + output_type = self._get_output_type(function_call["name"]) + + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException("This output parser can only be used with a chat generation.") + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + + if self.args_only: + _result = func_call["arguments"] + else: + _result = func_call + + try: + if self.args_only: + pydantic_args = output_type.model_validate_json(_result) + else: + pydantic_args = output_type.model_validate_json(_result["arguments"]) + except ValidationError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + return pydantic_args + + def _pre_parse_function_call(self, result: list[Generation]) -> dict: + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException("This output parser can only be used with a chat generation.") + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError: + raise OutputParserException( + "The model refused to respond with a " f"function call:\n{message.content}\n\n", + llm_output=msg_to_str(message), + ) + + return func_call + + def _get_output_type(self, function_name: str) -> Type[M]: + output_type_iter = filter(lambda t: t.__name__.lower() == function_name, self.output_types) + if output_type_iter is None: + raise OutputParserException(f"No parser found for function: {function_name}") + return next(output_type_iter) + + +P = TypeVar("P", bound=ParserBaseModel) + + +class CustomPydanticOutputParser(BaseOutputParser[P]): + pydantic_object: Type[P] + + def parse(self, text: str) -> P: + try: + return self.pydantic_object.parse(text) + except (json.JSONDecodeError, ValidationError) as e: + raise OutputParserException( + f"Failed to parse {self.pydantic_object.__name__} " f"from completion {text}. Got: {e}", + llm_output=text, + ) + + def get_format_instructions(self) -> str: + reduced_schema = self.pydantic_object.model_json_schema() + if "title" in reduced_schema: + del reduced_schema["title"] + if "type" in reduced_schema: + del reduced_schema["type"] + + return self.pydantic_object.format_instructions().format( + schema=json.dumps(reduced_schema), + ) + + @property + def _type(self) -> str: + return "pydantic" + + +class RetryPydanticOutputParser(BaseOutputParser[M]): + """Parse an output using a pydantic model.""" + + pydantic_object: Type[M] + """The pydantic model to parse.""" + + retry: int + retry_llm: BaseChatModel | str | None = None + + def parse(self, text: str) -> M: + try: + matches = re.findall(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) + if len(matches) > 1: + for match in matches: + try: + json_object = json.loads(match, strict=False) + return self.pydantic_object.model_validate(json_object) + except (json.JSONDecodeError, ValidationError): + continue + elif len(matches) == 1: + json_object = json.loads(matches[0], strict=False) + return self.pydantic_object.model_validate(json_object) + # no matches + raise OutputParserException( + f"No JSON {self.pydantic_object.__name__} found in completion {text}.", + llm_output=text, + ) + except (json.JSONDecodeError, ValidationError) as e: + if self.retry > 0: + print(f"Retrying parsing {self.pydantic_object.__name__}...") + return self.retry_chain.invoke( + input={"output": text, "error": str(e)}, + config={"run_name": "RetryPydanticOutputParser"}, + ) + # no retries left + raise OutputParserException(str(e), llm_output=text) + + def get_format_instructions(self) -> str: + schema = self.pydantic_object.model_json_schema() + + # Remove extraneous fields. + reduced_schema = schema + if "title" in reduced_schema: + del reduced_schema["title"] + if "type" in reduced_schema: + del reduced_schema["type"] + # Ensure json in context is well-formed with double quotes. + schema_str = yaml.dump(reduced_schema) + + return ( + "Please respond with a json result matching the following schema:" + f"\n\n```schema\n{schema_str}\n```\n" + "Do not repeat the schema. Only respond with the result." + ) + + @property + def _type(self) -> str: + return "pydantic" + + @property + def retry_chain(self) -> Runnable: + from ..syntax.executable import runnable + + return runnable( + instruction="Retry parsing the output by fixing the error.", + input_args=["output", "error"], + output_type=self.pydantic_object, + llm=self.retry_llm, + settings_override={"retry_parse": self.retry - 1}, + ) From 8b624dfbd1c22c5c755668c5f90999573e7322d9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 094/451] =?UTF-8?q?=F0=9F=93=84=20Add=20empty=20pydantic?= =?UTF-8?q?=20parser=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/pydantic.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/funcchain/parser/pydantic.py diff --git a/src/funcchain/parser/pydantic.py b/src/funcchain/parser/pydantic.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/funcchain/parser/pydantic.py @@ -0,0 +1 @@ + From 757af27af821b0e0f0f0a989d1979d806e6f2aaf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 095/451] =?UTF-8?q?=E2=9C=A8=20Add=20selector.py=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/selector.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/parser/selector.py diff --git a/src/funcchain/parser/selector.py b/src/funcchain/parser/selector.py new file mode 100644 index 0000000..e69de29 From 487bc0d1007af6d6e9c9abf06a33353d1c2c8651 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 096/451] =?UTF-8?q?=F0=9F=93=84=20Add=20empty=20schema=20?= =?UTF-8?q?=5F=5Finit=5F=5F.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/schema/__init__.py diff --git a/src/funcchain/schema/__init__.py b/src/funcchain/schema/__init__.py new file mode 100644 index 0000000..e69de29 From e2d04f6f989c2acf199bc781574d5714d275c16d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 097/451] =?UTF-8?q?=E2=9C=A8=20Add=20Signature=20schema=20?= =?UTF-8?q?class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/funcchain/schema/signature.py diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py new file mode 100644 index 0000000..3c5a351 --- /dev/null +++ b/src/funcchain/schema/signature.py @@ -0,0 +1,53 @@ +from typing import Any, Generic, TypeVar + +from langchain.pydantic_v1 import BaseModel, Field +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import BaseMessage + +from ..backend.settings import FuncchainSettings, settings + +T = TypeVar("T", bound=Any) + + +class Signature(BaseModel, Generic[T]): + """ + Fundamental structure of an executable prompt. + """ + + instruction: str + """ Prompt instruction to the language model. """ + + input_args: list[str] = Field(default_factory=list) + """ List of input arguments for the prompt template. """ + + output_type: type[T] = Field(default=str) + """ Type to parse the output into. """ + + # todo: should this be defined at compile time? maybe runtime is better + llm: BaseChatModel | str + """ Chat model to use as string or langchain object. """ + + # todo: is history really needed? maybe this could be a background optimization + history: list[BaseMessage] = Field(default_factory=list) + """ Additional messages that are inserted before the instruction. """ + + # update_history: bool = Field(default=True) + + settings: FuncchainSettings = Field(default=settings) + """ Local settings to override global settings. """ + + class Config: + arbitrary_types_allowed = True + + def __hash__(self) -> int: + """Hash for caching keys.""" + return hash( + ( + self.instruction, + tuple(self.input_args), + self.output_type, + self.llm, + tuple(self.history), + self.settings, + ) + ) From 965fcd90f37d5a24023900ab1b23ea5483d303dd Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 098/451] =?UTF-8?q?=E2=9C=A8=20Add=20syntax=20module=20sig?= =?UTF-8?q?nature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/funcchain/syntax/__init__.py diff --git a/src/funcchain/syntax/__init__.py b/src/funcchain/syntax/__init__.py new file mode 100644 index 0000000..0142f6e --- /dev/null +++ b/src/funcchain/syntax/__init__.py @@ -0,0 +1 @@ +# syntax -> signature From 61a1817df9a9c1b2d7f78d1b73be4a9739168fef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 099/451] =?UTF-8?q?=E2=9C=A8=20Add=20=5F=5Finit=5F=5F.py?= =?UTF-8?q?=20placeholder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/syntax/components/__init__.py diff --git a/src/funcchain/syntax/components/__init__.py b/src/funcchain/syntax/components/__init__.py new file mode 100644 index 0000000..e69de29 From 540c7e8834f03d5dcfa2e5342d95c39ca799e171 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 100/451] =?UTF-8?q?=E2=9C=A8=20Add=20chat=20routing=20comp?= =?UTF-8?q?onent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/router.py | 140 ++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/funcchain/syntax/components/router.py diff --git a/src/funcchain/syntax/components/router.py b/src/funcchain/syntax/components/router.py new file mode 100644 index 0000000..35b5de7 --- /dev/null +++ b/src/funcchain/syntax/components/router.py @@ -0,0 +1,140 @@ +from enum import Enum +from typing import Any, AsyncIterator, Callable, Iterator, Optional + +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.runnables import ( + RouterRunnable, + Runnable, + RunnableLambda, + RunnableSerializable, +) +from langchain_core.runnables.config import RunnableConfig +from typing_extensions import TypedDict + +from ...utils.msg_tools import msg_to_str +from ..executable import runnable + + +class Route(TypedDict): + handler: Callable | Runnable + description: str + + +Routes = dict[str, Route] + + +class RouterChat(Runnable[HumanMessage, AIMessage]): + """ + A router component that can be used to route user requests to different handlers. + """ + + def __init__( + self, + routes: Routes, + llm: Optional[BaseChatModel | str] = None, + history: Optional[BaseChatMessageHistory] = None, + add_default_handler: bool = True, + ) -> None: + self.routes = routes + self.llm = llm + self.history = history + + if add_default_handler: + self._add_default_handler() + + @property + def runnable(self) -> RunnableSerializable[HumanMessage, AIMessage]: + return { + "input": lambda x: x, + "output": { + "input": lambda x: x, + "key": { + # todo "images": x.images, + "user_request": msg_to_str, + "routes": lambda _: self._routes_repr(), + } + # route selection + | self._selector() + | RunnableLambda(lambda x: x.selector.value), + } + # route invocation + | RouterRunnable( + runnables={name: run["handler"] for name, run in self.routes.items()}, + ), + # update history + } | RunnableLambda( + lambda x: (self.history.add_message(x["input"]) or self.history.add_message(x["output"]) or x["output"]) + if self.history + else x["output"] + ) + + def _selector(self) -> Runnable[dict[str, str], Any]: + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) + from pydantic import BaseModel, Field + + class RouterModel(BaseModel): + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) + + return runnable( + instruction="Given the user request select the appropriate route.", + input_args=["user_request", "routes"], # todo: optional images + output_type=RouterModel, + context=self.history.messages if self.history else [], + llm=self.llm, + ) + + def _add_default_handler(self) -> None: + if "default" not in self.routes.keys(): + self.routes["default"] = { + "handler": ( + {"user_request": lambda x: msg_to_str(x)} + | runnable( + instruction="{user_request}", + input_args=["user_request"], + output_type=str, + llm=self.llm, + ) + | RunnableLambda(lambda x: AIMessage(content=x)) + ), + "description": ( + "Choose this for everything else like " + "normal questions or random things.\n" + "As example: 'How does this work?' or " + "'Whatsup' or 'What is the meaning of life?'" + ), + } + + def _routes_repr(self) -> str: + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) + + def invoke(self, input: HumanMessage, config: RunnableConfig | None = None) -> AIMessage: + return self.runnable.invoke(input, config=config) + + async def ainvoke(self, input: HumanMessage, config: RunnableConfig | None = None, **kwargs: Any) -> AIMessage: + return await self.runnable.ainvoke(input, config, **kwargs) + + def stream( + self, + input: HumanMessage, + config: RunnableConfig | None = None, + **kwargs: Any | None, + ) -> Iterator[AIMessage]: + yield from self.runnable.stream(input, config, **kwargs) + + async def astream( + self, + input: HumanMessage, + config: RunnableConfig | None = None, + **kwargs: Any | None, + ) -> AsyncIterator[AIMessage]: + async for msg in self.runnable.astream(input, config, **kwargs): + yield msg From 1157385dbca0a510b90cf814b8c5c9f7db8d0cc3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 101/451] =?UTF-8?q?=E2=9C=A8=20Add=20decorators.py=20modul?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/decorators.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/syntax/decorators.py diff --git a/src/funcchain/syntax/decorators.py b/src/funcchain/syntax/decorators.py new file mode 100644 index 0000000..e69de29 From 972449eb1748d866461db40be0c67fd0eb52e99a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 102/451] =?UTF-8?q?=E2=9C=A8=20Add=20funcchain=20execution?= =?UTF-8?q?=20utilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/funcchain/syntax/executable.py diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py new file mode 100644 index 0000000..1c1ea97 --- /dev/null +++ b/src/funcchain/syntax/executable.py @@ -0,0 +1,126 @@ +from typing import TypeVar + +from langchain.memory import ChatMessageHistory +from langchain_core.callbacks.base import Callbacks +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import BaseMessage, SystemMessage +from langchain_core.runnables import Runnable + +from ..backend.compiler import compile_chain +from ..backend.settings import SettingsOverride, create_local_settings +from ..schema.signature import Signature +from .meta_inspect import ( + from_docstring, + get_output_type, + get_parent_frame, + kwargs_from_parent, +) + +ChainOut = TypeVar("ChainOut") + + +def chain( + system: str | None = None, + instruction: str | None = None, + context: list[BaseMessage] = [], + memory: BaseChatMessageHistory | None = None, + settings_override: SettingsOverride = {}, + **input_kwargs: str, +) -> ChainOut: # type: ignore + """ + Generate response of llm for provided instructions. + """ + settings = create_local_settings(settings_override) + callbacks: Callbacks = None + output_type = get_output_type() + + memory = memory or ChatMessageHistory() + input_kwargs.update(kwargs_from_parent()) + system = system or settings.default_system_prompt + instruction = instruction or from_docstring() + + sig: Signature = Signature( + instruction=instruction, + input_args=list(input_kwargs.keys()), + output_type=output_type, + llm=settings_override.get("llm", settings.llm), + history=context, + settings=settings, + ) + chain: Runnable[dict[str, str], ChainOut] = compile_chain(sig) + result = chain.invoke(input_kwargs, {"run_name": get_parent_frame(3).function, "callbacks": callbacks}) + + if memory and isinstance(result, str): + # TODO: function calls? + memory.add_ai_message(result) + + return result + + +async def achain( + system: str | None = None, + instruction: str | None = None, + context: list[BaseMessage] = [], + memory: BaseChatMessageHistory | None = None, + settings_override: SettingsOverride = {}, + **input_kwargs: str, +) -> ChainOut: + """ + Asyncronously generate response of llm for provided instructions. + """ + settings = create_local_settings(settings_override) + callbacks: Callbacks = None + output_type = get_output_type() + + memory = memory or ChatMessageHistory() + input_kwargs.update(kwargs_from_parent()) + system = system or settings.default_system_prompt + instruction = instruction or from_docstring() + + sig: Signature = Signature( + instruction=instruction, + input_args=list(input_kwargs.keys()), + output_type=output_type, + llm=settings_override.get("llm", settings.llm), + history=context, + settings=settings, + ) + chain: Runnable[dict[str, str], ChainOut] = compile_chain(sig) + result = await chain.ainvoke(input_kwargs, {"run_name": get_parent_frame(5).function, "callbacks": callbacks}) + + if memory and isinstance(result, str): + # TODO: function calls? + memory.add_ai_message(result) + + return result + + +def runnable( + instruction: str, + output_type: type[ChainOut], + input_args: list[str] = [], + context: list = [], + llm: BaseChatModel | str | None = None, + system: str = "", + settings_override: SettingsOverride = {}, +) -> Runnable[dict[str, str], ChainOut]: + """ + Experimental replacement for using the funcchain syntax. + """ + if settings_override and llm: + settings_override["llm"] = llm + instruction = "\n" + instruction + settings = create_local_settings(settings_override) + context = [SystemMessage(content=system)] + context + + sig: Signature = Signature( + instruction=instruction, + input_args=input_args, + output_type=output_type, + llm=settings_override.get("llm", settings.llm), + history=context, + settings=settings, + ) + + return compile_chain(sig) From 4f865c7eab6475e4d3ba7f1f2d940f2f0589a56c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 103/451] =?UTF-8?q?=F0=9F=94=8D=20Rename=20meta=5Finspect.?= =?UTF-8?q?py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/meta_inspect.py | 95 ++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/funcchain/syntax/meta_inspect.py diff --git a/src/funcchain/syntax/meta_inspect.py b/src/funcchain/syntax/meta_inspect.py new file mode 100644 index 0000000..e575437 --- /dev/null +++ b/src/funcchain/syntax/meta_inspect.py @@ -0,0 +1,95 @@ +import types +from inspect import FrameInfo, currentframe, getouterframes +from typing import Union + +from langchain_core.language_models import BaseChatModel +from langchain_core.output_parsers import BaseOutputParser, StrOutputParser + +from ..parser.parsers import ( + BoolOutputParser, + ParserBaseModel, + RetryPydanticOutputParser, +) + +FUNC_DEPTH = 4 + + +def get_parent_frame(depth: int = FUNC_DEPTH) -> FrameInfo: + """ + Get the dep'th parent function information. + """ + return getouterframes(currentframe())[depth] + + +def get_func_obj() -> types.FunctionType: + """ + Get the parent caller function. + """ + func_name = get_parent_frame().function + if func_name == "": + raise RuntimeError("Cannot get function object from module") + if func_name == "": + raise RuntimeError("Cannot get function object from lambda") + + try: + func = get_parent_frame().frame.f_globals[func_name] + except KeyError: + func = get_parent_frame(FUNC_DEPTH + 1).frame.f_locals[func_name] + return func + + +def from_docstring() -> str: + """ + Get the docstring of the parent caller function. + """ + if doc_str := get_func_obj().__doc__: + return "\n".join([line.lstrip() for line in doc_str.split("\n")]) + raise ValueError(f"The funcchain ({get_parent_frame().function}) must have a docstring") + + +def get_output_type() -> type: + """ + Get the output type annotation of the parent caller function. + """ + try: + # print(get_parent_frame().frame.f_globals) + return get_func_obj().__annotations__["return"] + except KeyError: + raise ValueError("The funcchain must have a return type annotation") + + +def parser_for( + output_type: type, + retry: int, + llm: BaseChatModel | str | None = None, +) -> BaseOutputParser | None: + """ + Get the parser from the type annotation of the parent caller function. + """ + if isinstance(output_type, types.UnionType): + return None + # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) + if getattr(output_type, "__origin__", None) is Union: + output_type = output_type.__args__[0] # type: ignore + return None + # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) + if output_type is str: + return StrOutputParser() + if output_type is bool: + return BoolOutputParser() + if issubclass(output_type, ParserBaseModel): + return output_type.output_parser() # type: ignore + + from pydantic import BaseModel + + if issubclass(output_type, BaseModel): + return RetryPydanticOutputParser(pydantic_object=output_type, retry=retry, retry_llm=llm) + else: + raise RuntimeError(f"Output Type is not supported: {output_type}") + + +def kwargs_from_parent() -> dict[str, str]: + """ + Get the kwargs from the parent function. + """ + return get_parent_frame(FUNC_DEPTH - 1).frame.f_locals From 746a141d4909e669e5a764eba9216538c64dd673 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 104/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20types.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/types.py | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/funcchain/syntax/types.py diff --git a/src/funcchain/syntax/types.py b/src/funcchain/syntax/types.py new file mode 100644 index 0000000..5683ebf --- /dev/null +++ b/src/funcchain/syntax/types.py @@ -0,0 +1,88 @@ +import json +import re +from typing import Optional + +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers import BaseOutputParser +from pydantic import BaseModel, Field +from typing_extensions import Self + + +class ParserBaseModel(BaseModel): + @classmethod + def output_parser(cls) -> BaseOutputParser[Self]: + from ..parser.parsers import CustomPydanticOutputParser + + return CustomPydanticOutputParser(pydantic_object=cls) + + @classmethod + def parse(cls, text: str) -> Self: + """Override for custom parsing.""" + match = re.search(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) + json_str = "" + if match: + json_str = match.group() + json_object = json.loads(json_str, strict=False) + return cls.model_validate(json_object) + + @staticmethod + def format_instructions() -> str: + return ( + "Please respond with a json result matching the following schema:" + "\n\n```schema\n{schema}\n```\n" + "Do not repeat the schema. Only respond with the result." + ) + + @staticmethod + def custom_grammar() -> str | None: + return None + + +class CodeBlock(ParserBaseModel): + code: str + language: Optional[str] = None + + @classmethod + def parse(cls, text: str) -> "CodeBlock": + matches = re.finditer(r"```(?P\w+)?\n?(?P.*?)```", text, re.DOTALL) + for match in matches: + groupdict = match.groupdict() + groupdict["language"] = groupdict.get("language", None) + + # custom markdown fix + if groupdict["language"] == "markdown": + t = text.split("```markdown")[1] + return cls( + language="markdown", + code=t[: -(len(t.split("```")[-1]) + 3)], + ) + + return cls(**groupdict) + + return cls(code=text) # TODO: fix this hack + raise OutputParserException("Invalid codeblock") + + @staticmethod + def format_instructions() -> str: + return "Answer with a codeblock." + + @staticmethod + def custom_grammar() -> str | None: + return 'root ::= "```" ([^`] | "`" [^`] | "``" [^`])* "```"' + + def __str__(self) -> str: + return self.code + + +class Error(BaseModel): + """ + Fallback function for invalid input. + If you are unsure on what function to call, use this error function as fallback. + This will tell the user that the input is not valid. + """ + + title: str = Field(description="CamelCase Name titeling the error") + description: str = Field(..., description="Short description of the unexpected situation") + + def __raise__(self) -> None: + raise Exception(self.description) From 516ae090346b955fb6b96b46c4366fffd513bef9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 105/451] =?UTF-8?q?=F0=9F=A7=B9=20Cleaned=20=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/funcchain/utils/__init__.py b/src/funcchain/utils/__init__.py index 20c7876..e69de29 100644 --- a/src/funcchain/utils/__init__.py +++ b/src/funcchain/utils/__init__.py @@ -1,7 +0,0 @@ -from .decorators import * # noqa: F401, F403 -from .function_frame import * # noqa: F401, F403 -from .grammars import pydantic_to_grammar # noqa: F401, F403 -from .grammars import schema_to_grammar # noqa: F401, F403 -from .helpers import * # noqa: F401, F403 -from .image import * # noqa: F401, F403 -from .model_defaults import * # noqa: F401, F403 From e9e3b8011965032e60b5fe181ceb5853e27eee11 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 106/451] =?UTF-8?q?=F0=9F=94=80=20Renamed=20=5Fllms.py=20f?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/_llms.py | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/funcchain/utils/_llms.py diff --git a/src/funcchain/utils/_llms.py b/src/funcchain/utils/_llms.py new file mode 100644 index 0000000..e632774 --- /dev/null +++ b/src/funcchain/utils/_llms.py @@ -0,0 +1,43 @@ +from typing import Any, Dict, Optional + +from langchain.chat_models import ChatOllama as _ChatOllama +from langchain_core.pydantic_v1 import validator + + +class ChatOllama(_ChatOllama): + grammar: Optional[str] = None + """ + The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the model output. + """ + + @validator("grammar") + def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: + if v is not None and "root ::=" not in v: + raise ValueError("Grammar must contain a root rule.") + return v + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling Ollama.""" + return { + "model": self.model, + "format": self.format, + "options": { + "mirostat": self.mirostat, + "mirostat_eta": self.mirostat_eta, + "mirostat_tau": self.mirostat_tau, + "num_ctx": self.num_ctx, + "num_gpu": self.num_gpu, + "num_thread": self.num_thread, + "repeat_last_n": self.repeat_last_n, + "repeat_penalty": self.repeat_penalty, + "temperature": self.temperature, + "stop": self.stop, + "tfs_z": self.tfs_z, + "top_k": self.top_k, + "top_p": self.top_p, + "grammar": self.grammar, # added + }, + "system": self.system, + "template": self.template, + } From daef796269455d16276ed7ff900c1a275df773f4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 107/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20decor?= =?UTF-8?q?ators.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/decorators.py | 148 ------------------------------ 1 file changed, 148 deletions(-) delete mode 100644 src/funcchain/utils/decorators.py diff --git a/src/funcchain/utils/decorators.py b/src/funcchain/utils/decorators.py deleted file mode 100644 index 5286bf3..0000000 --- a/src/funcchain/utils/decorators.py +++ /dev/null @@ -1,148 +0,0 @@ -from asyncio import iscoroutinefunction -from asyncio import sleep as asleep -from functools import wraps -from time import sleep -from typing import Any - -from langchain.callbacks import get_openai_callback -from langchain.callbacks.openai_info import OpenAICallbackHandler -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.exceptions import OutputParserException -from langchain_core.messages import AIMessage -from rich import print - -from ..exceptions import ParsingRetryException -from ..settings import FuncchainSettings -from .function_frame import get_parent_frame - - -def retry_parse(fn: Any) -> Any: - """ - Retry parsing the output for a given number of times. - - Raises: - - OutputParserException: If the output cannot be parsed. - """ - if iscoroutinefunction(fn): - - @wraps(fn) - async def async_wrapper(*args: Any, **kwargs: Any) -> Any: - memory: BaseChatMessageHistory = args[3] - settings: FuncchainSettings = args[4] - retry = settings.retry_parse - for r in range(retry): - try: - return await fn(*args, **kwargs) - except ParsingRetryException as e: - _handle_error(e, r, retry, memory) - await asleep(settings.retry_parse_sleep + r) - except OutputParserException as e: - if e.llm_output: - _handle_error( - ParsingRetryException( - e.observation, - e.llm_output, - e.send_to_llm, - message=AIMessage(content=e.llm_output), - ), - r, - retry, - memory, - ) - sleep(settings.retry_parse_sleep + r) - else: - raise e - - return async_wrapper - - else: - - @wraps(fn) - def sync_wrapper(*args: Any, **kwargs: Any) -> Any: - memory: BaseChatMessageHistory = args[3] - settings: FuncchainSettings = args[4] - retry = settings.retry_parse - for r in range(retry): - try: - return fn(*args, **kwargs) - except ParsingRetryException as e: - _handle_error(e, r, retry, memory) - sleep(settings.retry_parse_sleep + r) - except OutputParserException as e: - if e.llm_output: - _handle_error( - ParsingRetryException( - e.observation, - e.llm_output, - e.send_to_llm, - message=AIMessage(content=e.llm_output), - ), - r, - retry, - memory, - ) - sleep(settings.retry_parse_sleep + r) - else: - raise e - - return sync_wrapper - - -def _handle_error( - e: ParsingRetryException, - r: int, - retry: int, - memory: BaseChatMessageHistory, -) -> None: - """handle output parser exception retry""" - print(f"[bright_black]Retrying due to:\n{e}[/bright_black]") - # remove last retry from memory - if isinstance(m := memory.messages[-1].content, str): - if m.startswith("I got this error:") and m.endswith("Can you retry?"): - memory.messages.pop(), memory.messages.pop() - - memory.add_message(e.message) - memory.add_user_message( - "I got this error when trying to parse your json:" - f"\n```\n{e}\n```\n" - "Can you rewrite it so I do not get this again?" - ) - - if r == retry - 1: - raise e - - -def log_openai_callback(fn: Any) -> Any: - if not iscoroutinefunction(fn): - - @wraps(fn) - def sync_wrapper(*args: Any, **kwargs: Any) -> Any: - with get_openai_callback() as cb: - result = fn(*args, **kwargs) - _log_cost(cb, name=get_parent_frame(4).function) - return result - - return sync_wrapper - - else: - - @wraps(fn) - async def async_wrapper(*args: Any, **kwargs: Any) -> Any: - with get_openai_callback() as cb: - result = await fn(*args, **kwargs) - _log_cost(cb, name=get_parent_frame(4).function) - return result - - return async_wrapper - - -def _log_cost(cb: OpenAICallbackHandler, name: str) -> None: - if cb.total_tokens != 0: - total_cost = f"/ {cb.total_cost:.3f}$ " if cb.total_cost > 0 else "" - if total_cost == "/ 0.000$ ": - total_cost = "/ 0.001$ " - print( - "[bright_black]" - f"{cb.total_tokens:05}T {total_cost}- {name}" - "[/bright_black]" - ) From 2929fbcbc7941ee6c4dc549f8904eeae896c848f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 108/451] =?UTF-8?q?=E2=9C=A8=20Add=20utility=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/funcs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/funcchain/utils/funcs.py diff --git a/src/funcchain/utils/funcs.py b/src/funcchain/utils/funcs.py new file mode 100644 index 0000000..7dcc68c --- /dev/null +++ b/src/funcchain/utils/funcs.py @@ -0,0 +1,11 @@ +from typing import NoReturn + +from tiktoken import encoding_for_model + + +def raiser(e: Exception | str) -> NoReturn: + raise e if isinstance(e, Exception) else Exception(e) + + +def count_tokens(text: str, model: str = "gpt-4") -> int: + return len(encoding_for_model(model).encode(text)) From eb11711e4585135f2bc591c3993903807b7d4a61 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 109/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20unused?= =?UTF-8?q?=20helpers.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/helpers.py | 159 --------------------------------- 1 file changed, 159 deletions(-) delete mode 100644 src/funcchain/utils/helpers.py diff --git a/src/funcchain/utils/helpers.py b/src/funcchain/utils/helpers.py deleted file mode 100644 index 6603fb9..0000000 --- a/src/funcchain/utils/helpers.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import Any, NoReturn, Type - -from docstring_parser import parse -from langchain.chat_models import ChatOpenAI, ChatOllama -from langchain_core.language_models import BaseChatModel -from langchain_core.messages import HumanMessage, SystemMessage -from pydantic import BaseModel -from tiktoken import encoding_for_model - - -def raiser(e: Exception | str) -> NoReturn: - raise e if isinstance(e, Exception) else Exception(e) - - -def count_tokens(text: str, model: str = "gpt-4") -> int: - return len(encoding_for_model(model).encode(text)) - - -verified_openai_function_models = [ - "gpt-4", - "gpt-4-0613", - "gpt-4-1106-preview", - "gpt-4-32k", - "gpt-4-32k-0613", - "gpt-3.5-turbo", - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-1106", - "gpt-3.5-turbo-16k", - "gpt-3.5-turbo-16k-0613", -] - -verified_openai_vision_models = [ - "gpt-4-vision-preview", -] - -verified_ollama_vision_models = [ - "llava", - "bakllava", -] - - -def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: - if not isinstance(llm, BaseChatModel): - return "base_model" - if isinstance(llm, ChatOpenAI): - if llm.model_name in verified_openai_vision_models: - return "vision_model" - if llm.model_name in verified_openai_function_models: - return "function_model" - try: - if func_check: - llm.predict_messages( - [ - SystemMessage( - content="This is a test message to see if the model can run functions." - ), - HumanMessage(content="Hello!"), - ], - functions=[ - { - "name": "print", - "description": "show the input", - "parameters": { - "properties": { - "__arg1": {"title": "__arg1", "type": "string"}, - }, - "required": ["__arg1"], - "type": "object", - }, - } - ], - ) - except Exception: - return "chat_model" - else: - return "function_model" - elif isinstance(llm, ChatOllama): - for model in verified_ollama_vision_models: - if llm.model in model: - return "vision_model" - - return "chat_model" - - -def is_function_model( - llm: BaseChatModel, -) -> bool: - return gather_llm_type(llm) == "function_model" - - -def is_vision_model( - llm: BaseChatModel, -) -> bool: - return gather_llm_type(llm) == "vision_model" - - -def _remove_a_key(d: dict, remove_key: str) -> None: - """Remove a key from a dictionary recursively""" - if isinstance(d, dict): - for key in list(d.keys()): - if key == remove_key and "type" in d.keys(): - del d[key] - else: - _remove_a_key(d[key], remove_key) - - -def pydantic_to_functions(pydantic_type: Type[BaseModel]) -> dict[str, Any]: - schema = pydantic_type.model_json_schema() - - docstring = parse(pydantic_type.__doc__ or "") - parameters = {k: v for k, v in schema.items() if k not in ("title", "description")} - - for param in docstring.params: - if (name := param.arg_name) in parameters["properties"] and ( - description := param.description - ): - if "description" not in parameters["properties"][name]: - parameters["properties"][name]["description"] = description - - parameters["type"] = "object" - - if "description" not in schema: - if docstring.short_description: - schema["description"] = docstring.short_description - else: - schema["description"] = ( - f"Correctly extracted `{pydantic_type.__name__.lower()}` with all " - f"the required parameters with correct types" - ) - - _remove_a_key(parameters, "title") - _remove_a_key(parameters, "additionalProperties") - - return { - "function_call": { - "name": pydantic_type.__name__.lower(), - }, - "functions": [ - { - "name": pydantic_type.__name__.lower(), - "description": schema["description"], - "parameters": parameters, - }, - ], - } - - -def multi_pydantic_to_functions( - pydantic_types: list[Type[BaseModel]], -) -> dict[str, Any]: - functions: list[dict[str, Any]] = [ - pydantic_to_functions(pydantic_type)["functions"][0] - for pydantic_type in pydantic_types - ] - - return { - "function_call": "auto", - "functions": functions, - } From 33012bc605c7f4b10584eaf1a362b9f903422200 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 110/451] =?UTF-8?q?=F0=9F=94=84=20Reorder=20import=20state?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/utils/image.py b/src/funcchain/utils/image.py index cfe282a..b4b5371 100644 --- a/src/funcchain/utils/image.py +++ b/src/funcchain/utils/image.py @@ -1,4 +1,4 @@ -from base64 import b64encode, b64decode +from base64 import b64decode, b64encode from io import BytesIO from PIL import Image From 6ccc6e1b0f76edfb70fc4a022cdf87741e04712c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 111/451] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20msg=5Ftools.p?= =?UTF-8?q?y=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/msg_tools.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/funcchain/utils/msg_tools.py b/src/funcchain/utils/msg_tools.py index a4799c6..365326f 100644 --- a/src/funcchain/utils/msg_tools.py +++ b/src/funcchain/utils/msg_tools.py @@ -1,6 +1,7 @@ from typing import Union -from langchain_core.messages import BaseMessageChunk, BaseMessage as _BaseMessage +from langchain_core.messages import BaseMessage as _BaseMessage +from langchain_core.messages import BaseMessageChunk BaseMessage = Union[_BaseMessage, BaseMessageChunk] @@ -9,11 +10,7 @@ def msg_images(msg: BaseMessage) -> list[str]: """Return a list of image URLs in the message content.""" if isinstance(msg.content, str): return [] - return [ - item["image_url"]["url"] - for item in msg.content - if isinstance(item, dict) and item["type"] == "image_url" - ] + return [item["image_url"]["url"] for item in msg.content if isinstance(item, dict) and item["type"] == "image_url"] def msg_to_str(msg: BaseMessage) -> str: From 560d02989beb1037c53172870c5b93a6b02efcb8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:29 +0100 Subject: [PATCH 112/451] =?UTF-8?q?=E2=9C=A8=20Add=20Pydantic=20schema=20u?= =?UTF-8?q?tils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/pydantic.py | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/funcchain/utils/pydantic.py diff --git a/src/funcchain/utils/pydantic.py b/src/funcchain/utils/pydantic.py new file mode 100644 index 0000000..fc8f089 --- /dev/null +++ b/src/funcchain/utils/pydantic.py @@ -0,0 +1,66 @@ +from typing import Any, Type + +from docstring_parser import parse +from pydantic import BaseModel + + +def _remove_a_key(d: dict, remove_key: str) -> None: + """Remove a key from a dictionary recursively""" + if isinstance(d, dict): + for key in list(d.keys()): + if key == remove_key and "type" in d.keys(): + del d[key] + else: + _remove_a_key(d[key], remove_key) + + +def pydantic_to_functions(pydantic_type: Type[BaseModel]) -> dict[str, Any]: + schema = pydantic_type.model_json_schema() + + docstring = parse(pydantic_type.__doc__ or "") + parameters = {k: v for k, v in schema.items() if k not in ("title", "description")} + + for param in docstring.params: + if (name := param.arg_name) in parameters["properties"] and (description := param.description): + if "description" not in parameters["properties"][name]: + parameters["properties"][name]["description"] = description + + parameters["type"] = "object" + + if "description" not in schema: + if docstring.short_description: + schema["description"] = docstring.short_description + else: + schema["description"] = ( + f"Correctly extracted `{pydantic_type.__name__.lower()}` with all " + f"the required parameters with correct types" + ) + + _remove_a_key(parameters, "title") + _remove_a_key(parameters, "additionalProperties") + + return { + "function_call": { + "name": pydantic_type.__name__.lower(), + }, + "functions": [ + { + "name": pydantic_type.__name__.lower(), + "description": schema["description"], + "parameters": parameters, + }, + ], + } + + +def multi_pydantic_to_functions( + pydantic_types: list[Type[BaseModel]], +) -> dict[str, Any]: + functions: list[dict[str, Any]] = [ + pydantic_to_functions(pydantic_type)["functions"][0] for pydantic_type in pydantic_types + ] + + return { + "function_call": "auto", + "functions": functions, + } From 7e38805f538bc73218f3df20ebc6caf1227bdc1d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 113/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20deprec?= =?UTF-8?q?ated=20=5Fllms.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/_llms.py | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/funcchain/_llms.py diff --git a/src/funcchain/_llms.py b/src/funcchain/_llms.py deleted file mode 100644 index 04c987e..0000000 --- a/src/funcchain/_llms.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any, Dict, Optional - -from langchain_core.pydantic_v1 import validator -from langchain.chat_models import ChatOllama as _ChatOllama - - -class ChatOllama(_ChatOllama): - grammar: Optional[str] = None - """The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the model output. """ - - @validator("grammar") - def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: - if v is not None and "root ::=" not in v: - raise ValueError("Grammar must contain a root rule.") - return v - - @property - def _default_params(self) -> Dict[str, Any]: - """Get the default parameters for calling Ollama.""" - return { - "model": self.model, - "format": self.format, - "options": { - "mirostat": self.mirostat, - "mirostat_eta": self.mirostat_eta, - "mirostat_tau": self.mirostat_tau, - "num_ctx": self.num_ctx, - "num_gpu": self.num_gpu, - "num_thread": self.num_thread, - "repeat_last_n": self.repeat_last_n, - "repeat_penalty": self.repeat_penalty, - "temperature": self.temperature, - "stop": self.stop, - "tfs_z": self.tfs_z, - "top_k": self.top_k, - "top_p": self.top_p, - "grammar": self.grammar, # added - }, - "system": self.system, - "template": self.template, - } From bd4f858be627a06fea3f091ad1cd3f4a160754a2 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 114/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20creation.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/creation.py | 321 -------------------------------- 1 file changed, 321 deletions(-) delete mode 100644 src/funcchain/chain/creation.py diff --git a/src/funcchain/chain/creation.py b/src/funcchain/chain/creation.py deleted file mode 100644 index 6a525c0..0000000 --- a/src/funcchain/chain/creation.py +++ /dev/null @@ -1,321 +0,0 @@ -from types import UnionType -from typing import Type, TypeVar - -from langchain_core.callbacks import Callbacks -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.language_models import BaseChatModel -from langchain_core.messages import AIMessage, BaseMessage, HumanMessage -from langchain_core.output_parsers import BaseOutputParser -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.runnables import RunnableSerializable -from PIL import Image -from pydantic import BaseModel - -from ..parser import MultiToolParser, ParserBaseModel, PydanticFuncParser -from ..settings import FuncchainSettings -from ..streaming import stream_handler -from ..utils import ( - count_tokens, - is_function_model, - is_vision_model, - multi_pydantic_to_functions, - parser_for, - pydantic_to_functions, - pydantic_to_grammar, - univeral_model_selector, -) -from .prompt import ( - HumanImageMessagePromptTemplate, - create_chat_prompt, - create_instruction_prompt, -) - -ChainOutput = TypeVar("ChainOutput") - - -# TODO: do patch instead of seperate creation -def create_union_chain( - output_type: UnionType, - instruction_prompt: HumanImageMessagePromptTemplate, - system: str, - memory: BaseChatMessageHistory, - context: list[BaseMessage], - llm: BaseChatModel, - input_kwargs: dict[str, str], -) -> RunnableSerializable[dict[str, str], BaseModel]: - """ - Compile a langchain runnable chain from the funcchain syntax. - """ - if not all(issubclass(t, BaseModel) for t in output_type.__args__): - raise RuntimeError( - "Funcchain union types are currently only supported for pydantic models." - ) - - output_types: list[Type[BaseModel]] = output_type.__args__ # type: ignore - output_type_names = [t.__name__ for t in output_types] - - input_kwargs[ - "format_instructions" - ] = f"Extract to one of these output types: {output_type_names}." - - functions = multi_pydantic_to_functions(output_types) - - llm = llm.bind(**functions) # type: ignore - - prompt = create_chat_prompt( - system, - instruction_prompt, - context=[ - *context, - HumanMessage(content="Can you use a function call for the next response?"), - AIMessage(content="Yeah I can do that, just tell me what you need!"), - ], - memory=memory, - ) - - return prompt | llm | MultiToolParser(output_types=output_types) - - -# TODO: do patch instead of seperate creation -def create_pydanctic_chain( - output_type: type[BaseModel], - prompt: ChatPromptTemplate, - llm: BaseChatModel, - input_kwargs: dict[str, str], -) -> RunnableSerializable[dict[str, str], BaseModel]: - # TODO: check these format_instructions - input_kwargs["format_instructions"] = f"Extract to {output_type.__name__}." - functions = pydantic_to_functions(output_type) - - llm = llm.bind(**functions) # type: ignore - - return prompt | llm | PydanticFuncParser(pydantic_schema=output_type) - - -def create_chain( - system: str, - instruction: str, - output_type: Type[ChainOutput], - context: list[BaseMessage], - memory: BaseChatMessageHistory, - settings: FuncchainSettings, - input_kwargs: dict[str, str], -) -> RunnableSerializable[dict[str, str], ChainOutput]: - """ - Compile a langchain runnable chain from the funcchain syntax. - """ - # large language model - _llm = _gather_llm(settings) - llm = _add_custom_callbacks(_llm, settings) - - parser = parser_for(output_type) - - # add format instructions for parser - f_instructions = None - if parser and (settings.streaming or not is_function_model(llm)): - # streaming behavior is not supported for function models - # but for normal function models we do not need to add format instructions - instruction, f_instructions = _add_format_instructions( - parser, - instruction, - input_kwargs, - ) - - # patch inputs - _crop_large_inputs( - system, - instruction, - input_kwargs, - settings, - ) - - # for vision models - images = _handle_images(llm, memory, input_kwargs) - - # create prompts - instruction_prompt = create_instruction_prompt( - instruction, - images, - input_kwargs, - format_instructions=f_instructions, - ) - chat_prompt = create_chat_prompt(system, instruction_prompt, context, memory) - - # add formatted instruction to chat history - memory.add_message(instruction_prompt.format(**input_kwargs)) - - _inject_grammar_for_local_models(llm, output_type) - - # function model patches - if is_function_model(llm): - if isinstance(output_type, UnionType): - return create_union_chain( - output_type, - instruction_prompt, - system, - memory, - context, - llm, - input_kwargs, - ) - - if issubclass(output_type, BaseModel) and not issubclass( - output_type, ParserBaseModel - ): - if settings.streaming and hasattr(llm, "model_kwargs"): - llm.model_kwargs = {"response_format": {"type": "json_object"}} - else: - return create_pydanctic_chain( # type: ignore - output_type, - chat_prompt, - llm, - input_kwargs, - ) - assert parser is not None - return chat_prompt | llm | parser - - -def _add_format_instructions( - parser: BaseOutputParser, - instruction: str, - input_kwargs: dict[str, str], -) -> tuple[str, str | None]: - """ - Add parsing format instructions - to the instruction message and input_kwargs - if the output parser supports it. - """ - try: - if format_instructions := parser.get_format_instructions(): - instruction += "\n{format_instructions}" - input_kwargs["format_instructions"] = format_instructions - return instruction, format_instructions - except NotImplementedError: - return instruction, None - - -def _crop_large_inputs( - system: str, - instruction: str, - input_kwargs: dict, - settings: FuncchainSettings, -) -> None: - """ - Crop large inputs to avoid exceeding the maximum number of tokens. - """ - base_tokens = count_tokens(instruction + system) - for k, v in input_kwargs.copy().items(): - if isinstance(v, str): - content_tokens = count_tokens(v) - if base_tokens + content_tokens > settings.context_lenght: - input_kwargs[k] = v[: (settings.context_lenght - base_tokens) * 2 // 3] - print("Truncated: ", len(input_kwargs[k])) - - -def _handle_images( - llm: BaseChatModel, - memory: BaseChatMessageHistory, - input_kwargs: dict[str, str], -) -> list[Image.Image]: - """ - Handle images for vision models. - """ - images = [v for v in input_kwargs.values() if isinstance(v, Image.Image)] - if is_vision_model(llm): - for k in list(input_kwargs.keys()): - if isinstance(input_kwargs[k], Image.Image): - del input_kwargs[k] - elif images: - raise RuntimeError("Images as input are only supported for vision models.") - elif _history_contains_images(memory): - print("Warning: Images in chat history are ignored for non-vision models.") - memory.messages = _clear_images_from_history(memory.messages) - - return images - - -def _inject_grammar_for_local_models(llm: BaseChatModel, output_type: type) -> None: - """ - Inject GBNF grammar into local models. - """ - try: - from funcchain._llms import ChatOllama - except: # noqa - pass - else: - if isinstance(llm, ChatOllama): - if isinstance(output_type, UnionType): - raise NotImplementedError( - "Union types are not yet supported for LlamaCpp models." - ) # TODO: implement - - if issubclass(output_type, BaseModel) and not issubclass( - output_type, ParserBaseModel - ): - llm.grammar = pydantic_to_grammar(output_type) - if issubclass(output_type, ParserBaseModel): - llm.grammar = output_type.custom_grammar() - - -def _gather_llm( - settings: FuncchainSettings, -) -> BaseChatModel: - if isinstance(settings.llm, BaseChatModel): - llm = settings.llm - else: - llm = univeral_model_selector(settings) - - if not llm: - raise RuntimeError( - "No language model provided. Either set the llm environment variable or " - "pass a model to the `chain` function." - ) - return llm - - -def _add_custom_callbacks( - llm: BaseChatModel, settings: FuncchainSettings -) -> BaseChatModel: - callbacks: Callbacks = [] - - if handler := stream_handler.get(): - callbacks = [handler] - - if settings.console_stream: - from ..streaming import AsyncStreamHandler - - callbacks = [ - AsyncStreamHandler(print, {"end": "", "flush": True}), - ] - - if callbacks: - settings.streaming = True - if hasattr(llm, "streaming"): - llm.streaming = True - llm.callbacks = callbacks - - return llm - - -def _history_contains_images(history: BaseChatMessageHistory) -> bool: - """ - Check if the chat history contains images. - """ - for message in history.messages: - if isinstance(message.content, list): - for content in message.content: - if isinstance(content, dict) and content.get("type") == "image_url": - return True - return False - - -def _clear_images_from_history(history: list[BaseMessage]) -> list[BaseMessage]: - """ - Remove images from the chat history. - """ - for message in history: - if isinstance(message.content, list): - for content in message.content: - if isinstance(content, dict) and content.get("type") == "image_url": - message.content.remove(content) - return history From 1b6a7b53bf79143343539b8d970e74772d3649e3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 115/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20prompt.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/chain/prompt.py | 176 ---------------------------------- 1 file changed, 176 deletions(-) delete mode 100644 src/funcchain/chain/prompt.py diff --git a/src/funcchain/chain/prompt.py b/src/funcchain/chain/prompt.py deleted file mode 100644 index 80dab50..0000000 --- a/src/funcchain/chain/prompt.py +++ /dev/null @@ -1,176 +0,0 @@ -from string import Formatter -from typing import Any, Optional, Type - -from langchain_core.chat_history import BaseChatMessageHistory -from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.prompts.chat import ( - BaseStringMessagePromptTemplate, - MessagePromptTemplateT, -) -from langchain_core.prompts.prompt import PromptTemplate -from PIL import Image -from pydantic import BaseModel - -from ..utils import image_to_base64_url - - -def create_instruction_prompt( - instruction: str, - images: list[Image.Image], - input_kwargs: dict[str, Any], - format_instructions: Optional[str] = None, -) -> "HumanImageMessagePromptTemplate": - template_format = _determine_format(instruction) - - required_f_str_vars = _extract_fstring_vars(instruction) - - _filter_fstring_vars(input_kwargs) - - inject_vars = [ - f"{var.upper()}:\n{{{var}}}\n" - for var, _ in input_kwargs.items() - if var not in required_f_str_vars - ] - added_instruction = "\n".join(inject_vars) - instruction = added_instruction + instruction - - images = [image_to_base64_url(image) for image in images] - - return HumanImageMessagePromptTemplate.from_template( - template=instruction, - template_format=template_format, - images=images, - partial_variables={"format_instructions": format_instructions} - if format_instructions - else None, - ) - - -def create_chat_prompt( - system: str, - instruction_template: "HumanImageMessagePromptTemplate", - context: list[BaseMessage], - memory: BaseChatMessageHistory, -) -> ChatPromptTemplate: - """ - Compose a chat prompt from a system message, - context and instruction template. - """ - # remove leading system message in case to not have two - if system and memory.messages and isinstance(memory.messages[0], SystemMessage): - memory.messages.pop(0) - - if memory.messages and isinstance(memory.messages[-1], HumanMessage): - return ChatPromptTemplate.from_messages( - [ - *([SystemMessage(content=system)] if system else []), - *memory.messages, - *context, - ] - ) - - return ChatPromptTemplate.from_messages( - [ - *([SystemMessage(content=system)] if system else []), - *memory.messages, - *context, - instruction_template, - ] - ) - - -def _determine_format( - instruction: str, -) -> str: - return "jinja2" if "{{" in instruction or "{%" in instruction else "f-string" - - -def _extract_fstring_vars(template: str) -> list[str]: - """ - TODO: enable jinja2 check - Function to extract f-string variables from a string. - """ - return [ - field_name # print("field_name:", field_name) or field_name.split(".")[0] - for _, field_name, _, _ in Formatter().parse(template) - if field_name is not None - ] - - -def _filter_fstring_vars( - input_kwargs: dict[str, Any], -) -> None: - """Mutate input_kwargs to filter out non-string values.""" - keys_to_remove = [ - key - for key, value in input_kwargs.items() - if not ( - isinstance(value, str) or isinstance(value, BaseModel) - ) # TODO: remove BaseModel - ] - for key in keys_to_remove: - del input_kwargs[key] - - -class HumanImageMessagePromptTemplate(BaseStringMessagePromptTemplate): - """Human message prompt template. This is a message sent from the user.""" - - images: list[str] = [] - - def format(self, **kwargs: Any) -> BaseMessage: - """Format the prompt template. - - Args: - **kwargs: Keyword arguments to use for formatting. - - Returns: - Formatted message. - """ - text = self.prompt.format(**kwargs) - return HumanMessage( - content=[ - { - "type": "text", - "text": text, - }, - *[ - { - "type": "image_url", - "image_url": { - "url": image, - "detail": "auto", - }, - } - for image in self.images - ], - ], - additional_kwargs=self.additional_kwargs, - ) - - @classmethod - def from_template( - cls: Type[MessagePromptTemplateT], - template: str, - template_format: str = "f-string", - partial_variables: Optional[dict[str, Any]] = None, - images: list[str] = [], - **kwargs: Any, - ) -> MessagePromptTemplateT: - """Create a class from a string template. - - Args: - template: a template. - template_format: format of the template. - **kwargs: keyword arguments to pass to the constructor. - - Returns: - A new instance of this class. - """ - prompt = PromptTemplate.from_template( - template, - template_format=template_format, - partial_variables=partial_variables, - ) - kwargs["images"] = images - return cls(prompt=prompt, **kwargs) From 10d59b56e0d9485bb253e2aed5d4562c58d11826 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 116/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20funcch?= =?UTF-8?q?ain/parser.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser.py | 243 ---------------------------------------- 1 file changed, 243 deletions(-) delete mode 100644 src/funcchain/parser.py diff --git a/src/funcchain/parser.py b/src/funcchain/parser.py deleted file mode 100644 index 6517565..0000000 --- a/src/funcchain/parser.py +++ /dev/null @@ -1,243 +0,0 @@ -import copy -import json -import yaml # type: ignore -import re -from typing import Callable, Optional, Type, TypeVar - -from langchain_core.exceptions import OutputParserException -from langchain_core.messages import AIMessage -from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser -from langchain_core.outputs import ChatGeneration, Generation -from pydantic import BaseModel, ValidationError - -from .exceptions import ParsingRetryException -from .types import CodeBlock as CodeBlock -from .types import ParserBaseModel - -T = TypeVar("T") - - -class LambdaOutputParser(BaseOutputParser[T]): - _parse: Optional[Callable[[str], T]] = None - - def parse(self, text: str) -> T: - if self._parse is None: - raise NotImplementedError( - "LambdaOutputParser.lambda_parse() is not implemented" - ) - return self._parse(text) - - @property - def _type(self) -> str: - return "lambda" - - -class BoolOutputParser(BaseOutputParser[bool]): - def parse(self, text: str) -> bool: - return text.strip()[:1].lower() == "y" - - def get_format_instructions(self) -> str: - return "\nAnswer only with 'Yes' or 'No'." - - @property - def _type(self) -> str: - return "bool" - - -M = TypeVar("M", bound=BaseModel) - - -class PydanticFuncParser(BaseGenerationOutputParser[M]): - pydantic_schema: Type[M] - args_only: bool = False - - def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation.", - ) - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise ParsingRetryException( - f"Could not parse function call: {exc}", - message=message, - ) - - if self.args_only: - _result = func_call["arguments"] - else: - _result = func_call - try: - if self.args_only: - pydantic_args = self.pydantic_schema.model_validate_json(_result) - else: - pydantic_args = self.pydantic_schema.model_validate_json( - _result["arguments"] - ) - except ValidationError as exc: - raise ParsingRetryException( - f"Could not parse function call: {exc}", message=message - ) - return pydantic_args - - -class MultiToolParser(BaseGenerationOutputParser[M]): - output_types: list[Type[M]] - args_only: bool = False - - def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: - function_call = self._pre_parse_function_call(result) - - output_type_names = [t.__name__.lower() for t in self.output_types] - - if function_call["name"] not in output_type_names: - raise OutputParserException("Invalid function call") - - print(function_call["name"]) - - output_type = self._get_output_type(function_call["name"]) - - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation." - ) - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise ParsingRetryException( - f"Could not parse function call: {exc}", message=message - ) - - if self.args_only: - _result = func_call["arguments"] - else: - _result = func_call - - try: - if self.args_only: - pydantic_args = output_type.model_validate_json(_result) - else: - pydantic_args = output_type.model_validate_json(_result["arguments"]) - except ValidationError as exc: - raise ParsingRetryException( - f"Could not parse function call: {exc}", - message=message, - ) - return pydantic_args - - def _pre_parse_function_call(self, result: list[Generation]) -> dict: - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation." - ) - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError: - raise ParsingRetryException( - f"The model refused to respond with a function call:\n{message.content}\n\n", - message=message, - ) - - return func_call - - def _get_output_type(self, function_name: str) -> Type[M]: - output_type_iter = filter( - lambda t: t.__name__.lower() == function_name, self.output_types - ) - if output_type_iter is None: - raise OutputParserException( - f"No parser found for function: {function_name}" - ) - return next(output_type_iter) - - -P = TypeVar("P", bound=ParserBaseModel) - - -class CustomPydanticOutputParser(BaseOutputParser[P]): - pydantic_object: Type[P] - - def parse(self, text: str) -> P: - try: - return self.pydantic_object.parse(text) - except (json.JSONDecodeError, ValidationError) as e: - raise ParsingRetryException( - f"Failed to parse {self.pydantic_object.__name__} from completion {text}. Got: {e}", - message=AIMessage(content=text), - ) - - def get_format_instructions(self) -> str: - reduced_schema = self.pydantic_object.model_json_schema() - if "title" in reduced_schema: - del reduced_schema["title"] - if "type" in reduced_schema: - del reduced_schema["type"] - - return self.pydantic_object.format_instructions().format( - schema=json.dumps(reduced_schema), - ) - - @property - def _type(self) -> str: - return "pydantic" - - -class PydanticOutputParser(BaseOutputParser[M]): - """Parse an output using a pydantic model.""" - - pydantic_object: Type[M] - """The pydantic model to parse.""" - - def parse(self, text: str) -> M: - try: - matches = re.findall( - r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL - ) - if len(matches) > 1: - for match in matches: - try: - json_object = json.loads(match, strict=False) - return self.pydantic_object.model_validate(json_object) - except (json.JSONDecodeError, ValidationError): - continue - elif len(matches) == 1: - json_object = json.loads(matches[0], strict=False) - return self.pydantic_object.model_validate(json_object) - raise ParsingRetryException( - f"Failed to parse {self.pydantic_object.__name__} from completion {text}.", - message=AIMessage(content=text), - ) - except (json.JSONDecodeError, ValidationError) as e: - raise ParsingRetryException( - str(e), - message=AIMessage(content=text), - ) - - def get_format_instructions(self) -> str: - schema = self.pydantic_object.model_json_schema() - - # Remove extraneous fields. - reduced_schema = schema - if "title" in reduced_schema: - del reduced_schema["title"] - if "type" in reduced_schema: - del reduced_schema["type"] - # Ensure json in context is well-formed with double quotes. - schema_str = yaml.dump(reduced_schema) - - return ( - "Please respond with a json result matching the following schema:" - f"\n\n```schema\n{schema_str}\n```\n" - "Do not repeat the schema. Only respond with the result." - ) - - @property - def _type(self) -> str: - return "pydantic" From 583d39634f87a2e5bbc08015d6bc96076fc35208 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 117/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20settin?= =?UTF-8?q?gs.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/settings.py | 93 --------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 src/funcchain/settings.py diff --git a/src/funcchain/settings.py b/src/funcchain/settings.py deleted file mode 100644 index c79d3ab..0000000 --- a/src/funcchain/settings.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Funcchain Settings: -Automatically loads environment variables from .env file -""" -from typing import Optional -from typing_extensions import TypedDict - -from langchain_core.language_models import BaseChatModel -from pydantic import Field -from pydantic_settings import BaseSettings - - -class FuncchainSettings(BaseSettings): - debug: bool = True - - llm: BaseChatModel | str = Field( - default="openai/gpt-3.5-turbo-1106", - validate_default=False, - ) - - console_stream: bool = False - - default_system_prompt: str = "" - - retry_parse: int = 5 - retry_parse_sleep: float = 0.1 - - # KEYS - openai_api_key: Optional[str] = None - azure_api_key: Optional[str] = None - anthropic_api_key: Optional[str] = None - google_api_key: Optional[str] = None - - # MODEL KWARGS - verbose: bool = False - streaming: bool = False - max_tokens: int = 2048 - temperature: float = 0.1 - - # OLLAMA KWARGS - context_lenght: int = 8196 - n_gpu_layers: int = 50 - keep_loaded: bool = False - - def model_kwargs(self) -> dict: - return { - "verbose": self.verbose, - "temperature": self.temperature, - "max_tokens": self.max_tokens, - "streaming": self.streaming, - } - - def openai_kwargs(self) -> dict: - return { - "openai_api_key": self.openai_api_key, - } - - def ollama_kwargs(self) -> dict: - return { - "n_ctx": self.context_lenght, - "use_mlock": self.keep_loaded, - "n_gpu_layers": self.n_gpu_layers, - } - - -settings = FuncchainSettings() - - -class SettingsOverride(TypedDict, total=False): - llm: BaseChatModel | str | None - - verbose: bool - temperature: float - max_tokens: int - streaming: bool - context_lenght: int - - -def get_settings(override: Optional[SettingsOverride] = None) -> FuncchainSettings: - if override: - if override["llm"] is None: - override["llm"] = settings.llm - return settings.model_copy(update=dict(override)) - return settings - - -# load langsmith logging vars -try: - import dotenv -except ImportError: - pass -else: - dotenv.load_dotenv() From d5b4d4d4e871cd0cccdd248992f339536b13ac1b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 118/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20deprec?= =?UTF-8?q?ated=20streaming.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/streaming.py | 117 ------------------------------------- 1 file changed, 117 deletions(-) delete mode 100644 src/funcchain/streaming.py diff --git a/src/funcchain/streaming.py b/src/funcchain/streaming.py deleted file mode 100644 index 259ac31..0000000 --- a/src/funcchain/streaming.py +++ /dev/null @@ -1,117 +0,0 @@ -from contextlib import asynccontextmanager, contextmanager -from contextvars import ContextVar -from typing import Any, AsyncGenerator, Awaitable, Callable, Coroutine, Generator -from uuid import UUID - -from langchain_core.callbacks.base import AsyncCallbackHandler -from langchain_core.messages import BaseMessage -from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult - - -class AsyncStreamHandler(AsyncCallbackHandler): - """Async callback handler that can be used to handle callbacks from langchain_core.""" - - def __init__( - self, fn: Callable[[str], Awaitable[None] | None], default_kwargs: dict - ) -> None: - self.fn = fn - self.default_kwargs = default_kwargs - self.cost: float = 0.0 - self.tokens: int = 0 - - async def on_chat_model_start( - self, - serialized: dict[str, Any], - messages: list[list[BaseMessage]], - *, - run_id: UUID, - parent_run_id: UUID | None = None, - tags: list[str] | None = None, - metadata: dict[str, Any] | None = None, - **kwargs: Any, - ) -> Any: - # from .utils import count_tokens - # for lists in messages: - # for message in lists: - # if message.content: - # if isinstance(message.content, str): - # self.tokens += count_tokens(message.content) - # elif isinstance(message.content, list): - # print("token_counting", message.content) - # # self.tokens += count_tokens(message) - pass - - async def on_llm_new_token( - self, - token: str, - *, - chunk: GenerationChunk | ChatGenerationChunk | None = None, - run_id: UUID, - parent_run_id: UUID | None = None, - tags: list[str] | None = None, - **kwargs: Any, - ) -> None: - if isinstance(self.fn, Coroutine): - await self.fn(token, **self.default_kwargs) - else: - self.fn(token, **self.default_kwargs) - - async def on_llm_end( - self, - response: LLMResult, - *, - run_id: UUID, - parent_run_id: UUID | None = None, - tags: list[str] | None = None, - **kwargs: Any, - ) -> None: - if self.fn is print: - print("\n") - - -stream_handler: ContextVar[AsyncStreamHandler | None] = ContextVar( - "stream_handler", default=None -) - - -@contextmanager -def stream_to( - fn: Callable[[str], None], **kwargs: Any -) -> Generator[AsyncStreamHandler, None, None]: - """ - Stream the llm tokens to a given function. - - Example: - >>> with stream_to(print): - ... # your chain calls here - """ - import builtins - - import rich - - if (fn is builtins.print or fn is rich.print) and kwargs == {}: - kwargs = {"end": "", "flush": True} - - cb = AsyncStreamHandler(fn, kwargs) - stream_handler.set(cb) - yield cb - stream_handler.set(None) - - -@asynccontextmanager -async def astream_to( - fn: Callable[[str], Awaitable[None] | None], **kwargs: Any -) -> AsyncGenerator[AsyncStreamHandler, None]: - """ - Asyncronously stream the llm tokens to a given function. - - Example: - >>> async with astream_to(print): - ... # your chain calls here - """ - if fn is print and kwargs == {}: - kwargs = {"end": "", "flush": True} - cb = AsyncStreamHandler(fn, kwargs) - stream_handler.set(cb) - yield cb - stream_handler.set(None) From 5adcdf12baa2ef232f533108ca70f2f11cf40da7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 119/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20types.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/types.py | 94 ------------------------------------------ 1 file changed, 94 deletions(-) delete mode 100644 src/funcchain/types.py diff --git a/src/funcchain/types.py b/src/funcchain/types.py deleted file mode 100644 index c0e1ec9..0000000 --- a/src/funcchain/types.py +++ /dev/null @@ -1,94 +0,0 @@ -import json -import re -from typing import Optional - -from langchain_core.exceptions import OutputParserException -from langchain_core.output_parsers import BaseOutputParser -from pydantic import BaseModel, Field -from typing_extensions import Self - - -class ParserBaseModel(BaseModel): - @classmethod - def output_parser(cls) -> BaseOutputParser[Self]: - from .parser import CustomPydanticOutputParser - - return CustomPydanticOutputParser(pydantic_object=cls) - - @classmethod - def parse(cls, text: str) -> Self: - """Override for custom parsing.""" - match = re.search( - r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL - ) - json_str = "" - if match: - json_str = match.group() - json_object = json.loads(json_str, strict=False) - return cls.model_validate(json_object) - - @staticmethod - def format_instructions() -> str: - return ( - "Please respond with a json result matching the following schema:" - "\n\n```schema\n{schema}\n```\n" - "Do not repeat the schema. Only respond with the result." - ) - - @staticmethod - def custom_grammar() -> str | None: - return None - - -class CodeBlock(ParserBaseModel): - code: str - language: Optional[str] = None - - @classmethod - def parse(cls, text: str) -> "CodeBlock": - matches = re.finditer( - r"```(?P\w+)?\n?(?P.*?)```", text, re.DOTALL - ) - for match in matches: - groupdict = match.groupdict() - groupdict["language"] = groupdict.get("language", None) - - # custom markdown fix - if groupdict["language"] == "markdown": - t = text.split("```markdown")[1] - return cls( - language="markdown", - code=t[: -(len(t.split("```")[-1]) + 3)], - ) - - return cls(**groupdict) - - return cls(code=text) # TODO: fix this hack - raise OutputParserException("Invalid codeblock") - - @staticmethod - def format_instructions() -> str: - return "Answer with a codeblock." - - @staticmethod - def custom_grammar() -> str | None: - return 'root ::= "```" ([^`] | "`" [^`] | "``" [^`])* "```"' - - def __str__(self) -> str: - return self.code - - -class Error(BaseModel): - """ - Fallback function for invalid input. - If you are unsure on what function to call, use this error function as fallback. - This will tell the user that the input is not valid. - """ - - title: str = Field(description="CamelCase Name titeling the error") - description: str = Field( - ..., description="Short description of the unexpected situation" - ) - - def __raise__(self) -> None: - raise Exception(self.description) From e52512bcecc73aa232fb619893a02b8dbf1bd650 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 120/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20functi?= =?UTF-8?q?on=5Fframe.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/function_frame.py | 88 --------------------------- 1 file changed, 88 deletions(-) delete mode 100644 src/funcchain/utils/function_frame.py diff --git a/src/funcchain/utils/function_frame.py b/src/funcchain/utils/function_frame.py deleted file mode 100644 index 3382a27..0000000 --- a/src/funcchain/utils/function_frame.py +++ /dev/null @@ -1,88 +0,0 @@ -import types -from inspect import FrameInfo, currentframe, getouterframes -from typing import Union - -from langchain_core.output_parsers import BaseOutputParser, StrOutputParser - -from ..parser import BoolOutputParser, ParserBaseModel, PydanticOutputParser - -FUNC_DEPTH = 7 - - -def get_parent_frame(depth: int = FUNC_DEPTH) -> FrameInfo: - """ - Get the dep'th parent function information. - """ - return getouterframes(currentframe())[depth] - - -def get_func_obj() -> types.FunctionType: - """ - Get the parent caller function. - """ - func_name = get_parent_frame().function - if func_name == "": - raise RuntimeError("Cannot get function object from module") - if func_name == "": - raise RuntimeError("Cannot get function object from lambda") - - try: - func = get_parent_frame().frame.f_globals[func_name] - except KeyError: - func = get_parent_frame(FUNC_DEPTH + 1).frame.f_locals[func_name] - return func - - -def from_docstring() -> str: - """ - Get the docstring of the parent caller function. - """ - if doc_str := get_func_obj().__doc__: - return "\n".join([line.lstrip() for line in doc_str.split("\n")]) - raise ValueError( - f"The funcchain ({get_parent_frame().function}) must have a docstring" - ) - - -def get_output_type() -> type: - """ - Get the output type annotation of the parent caller function. - """ - try: - # print(get_parent_frame().frame.f_globals) - return get_func_obj().__annotations__["return"] - except KeyError: - raise ValueError("The funcchain must have a return type annotation") - - -def parser_for(output_type: type) -> BaseOutputParser | None: - """ - Get the parser from the type annotation of the parent caller function. - """ - if isinstance(output_type, types.UnionType): - return None - # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) - if getattr(output_type, "__origin__", None) is Union: - output_type = output_type.__args__[0] # type: ignore - return None - # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) - if output_type is str: - return StrOutputParser() - if output_type is bool: - return BoolOutputParser() - if issubclass(output_type, ParserBaseModel): - return output_type.output_parser() # type: ignore - - from pydantic import BaseModel - - if issubclass(output_type, BaseModel): - return PydanticOutputParser(pydantic_object=output_type) - else: - raise RuntimeError(f"Output Type is not supported: {output_type}") - - -def kwargs_from_parent() -> dict[str, str]: - """ - Get the kwargs from the parent function. - """ - return get_parent_frame(FUNC_DEPTH - 1).frame.f_locals From 56316902e6300362a17a398016848a036ebdc174 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 121/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20obsol?= =?UTF-8?q?ete=20grammars.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/grammars.py | 138 -------------------------------- 1 file changed, 138 deletions(-) delete mode 100644 src/funcchain/utils/grammars.py diff --git a/src/funcchain/utils/grammars.py b/src/funcchain/utils/grammars.py deleted file mode 100644 index 7f48d86..0000000 --- a/src/funcchain/utils/grammars.py +++ /dev/null @@ -1,138 +0,0 @@ -import json -import re -from typing import Type - -from pydantic import BaseModel - -SPACE_RULE = '" "?' - -PRIMITIVE_RULES = { - "boolean": '("true" | "false") space', - "number": '("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space', - "integer": '("-"? ([0-9] | [1-9] [0-9]*)) space', - "string": r""" "\"" ( - [^"\\] | - "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) - )* "\"" space """, - "null": '"null" space', -} - -INVALID_RULE_CHARS_RE = re.compile(r"[^a-zA-Z0-9-]+") -GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"]') -GRAMMAR_LITERAL_ESCAPES = {"\r": "\\r", "\n": "\\n", '"': '\\"'} - - -class SchemaConverter: - def __init__(self, prop_order: dict, defs: dict) -> None: - self._prop_order = prop_order - self._defs = defs - self._rules = {"space": SPACE_RULE} - - def _format_literal(self, literal: str) -> str: - escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub( - lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)), # type: ignore - json.dumps(literal), - ) - return f'"{escaped}"' - - def _add_rule(self, name: str, rule: str) -> str: - esc_name = INVALID_RULE_CHARS_RE.sub("-", name) - if esc_name not in self._rules or self._rules[esc_name] == rule: - key = esc_name - else: - i = 0 - while f"{esc_name}{i}" in self._rules: - i += 1 - key = f"{esc_name}{i}" - self._rules[key] = rule - return key - - def visit(self, schema: dict, name: str) -> str: - schema_type = schema.get("type") - rule_name = name or "root" - - if "$ref" in schema: - ref_name = schema["$ref"].split("/")[-1] - assert ref_name in self._defs, f"Unresolved reference: {schema['$ref']}" - return self.visit(self._defs[ref_name], ref_name) - - elif "oneOf" in schema or "anyOf" in schema: - rule = " | ".join( - ( - self.visit(alt_schema, f'{name}{"-" if name else ""}{i}') - for i, alt_schema in enumerate( - schema.get("oneOf") or schema["anyOf"] - ) - ) - ) - return self._add_rule(rule_name, rule) - - elif "allOf" in schema: - rule = " ".join( - ( - self.visit(sub_schema, f'{name}{"-" if name else ""}{i}') - for i, sub_schema in enumerate(schema["allOf"]) - ) - ) - return self._add_rule(rule_name, rule) - - elif "const" in schema: - return self._add_rule(rule_name, self._format_literal(schema["const"])) - - elif "enum" in schema: - rule = " | ".join((self._format_literal(v) for v in schema["enum"])) - return self._add_rule(rule_name, rule) - - elif schema_type == "object" and "properties" in schema: - # TODO: `required` keyword - prop_order = self._prop_order - prop_pairs = sorted( - schema["properties"].items(), - # sort by position in prop_order (if specified) then by key - key=lambda kv: (prop_order.get(kv[0], len(prop_order)), kv[0]), - ) - - rule = '"{" space' - for i, (prop_name, prop_schema) in enumerate(prop_pairs): - prop_rule_name = self.visit( - prop_schema, f'{name}{"-" if name else ""}{prop_name}' - ) - if i > 0: - rule += ' "," space' - rule += rf' {self._format_literal(prop_name)} space ":" space {prop_rule_name}' - rule += ' "}" space' - - return self._add_rule(rule_name, rule) - - elif schema_type == "array" and "items" in schema: - # TODO `prefixItems` keyword - item_rule_name = self.visit( - schema["items"], f'{name}{"-" if name else ""}item' - ) - rule = ( - f'"[" space ({item_rule_name} ("," space {item_rule_name})*)? "]" space' - ) - return self._add_rule(rule_name, rule) - - else: - assert schema_type in PRIMITIVE_RULES, f"Unrecognized schema: {schema}" - return self._add_rule( - "root" if rule_name == "root" else schema_type, - PRIMITIVE_RULES[schema_type], - ) - - def format_grammar(self) -> str: - return "\n".join((f"{name} ::= {rule}" for name, rule in self._rules.items())) - - -def schema_to_grammar(json_schema: dict) -> str: - schema = json_schema - prop_order = {name: idx for idx, name in enumerate(schema["properties"].keys())} - defs = schema.get("$defs", {}) - converter = SchemaConverter(prop_order, defs) - converter.visit(schema, "") - return converter.format_grammar() - - -def pydantic_to_grammar(model: Type[BaseModel]) -> str: - return schema_to_grammar(model.model_json_schema()) From 31a5255f7628e1094c21f6349893cfb9c5fc215d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 10 Jan 2024 21:53:38 +0100 Subject: [PATCH 122/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20model?= =?UTF-8?q?=5Fdefaults.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/model_defaults.py | 106 -------------------------- 1 file changed, 106 deletions(-) delete mode 100644 src/funcchain/utils/model_defaults.py diff --git a/src/funcchain/utils/model_defaults.py b/src/funcchain/utils/model_defaults.py deleted file mode 100644 index 5791e31..0000000 --- a/src/funcchain/utils/model_defaults.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import Any - -from langchain_core.language_models import BaseChatModel - -from ..settings import FuncchainSettings - - -def univeral_model_selector( - settings: FuncchainSettings, - **model_kwargs: Any, -) -> BaseChatModel: - """ - Automatically selects the best possible model for a given ModelName. - You can use this schema: - - "provider/model_name:" - - and automatically select the right model for you. - You can add optional model kwargs like temperature. - - Examples: - - "openai/gpt-3.5-turbo" - - "anthropic/claude-2" - - "ollama/deepseek-llm-7b-chat" - - Supported: - [ openai, anthropic, google, ollama ] - - Raises: - - ModelNotFoundError, when the model is not found. - """ - model_name = settings.llm if isinstance(settings.llm, str) else "" - model_kwargs.update(settings.model_kwargs()) - - if model_name: - mtype, name = model_name.split("/") if "/" in model_name else ("", model_name) - mtype = mtype.lower() - - model_kwargs["model_name"] = name - - try: - match mtype: - case "openai": - from langchain.chat_models import ChatOpenAI - - model_kwargs.update(settings.openai_kwargs()) - return ChatOpenAI(**model_kwargs) - - case "anthropic": - from langchain.chat_models import ChatAnthropic - - return ChatAnthropic(**model_kwargs) - - case "google": - from langchain.chat_models import ChatGooglePalm - - return ChatGooglePalm(**model_kwargs) - - case "ollama": - from .._llms import ChatOllama - - model = model_kwargs.pop("model_name") - model_kwargs.update(settings.ollama_kwargs()) - return ChatOllama(model=model, **model_kwargs) - - except Exception as e: - print("ERROR:", e) - raise e - - try: - if "gpt-4" in name or "gpt-3.5" in name: - from langchain.chat_models import ChatOpenAI - - model_kwargs.update(settings.openai_kwargs()) - return ChatOpenAI(**model_kwargs) - - except Exception as e: - print(e) - - model_kwargs.pop("model_name") - - if settings.openai_api_key: - from langchain.chat_models import ChatOpenAI - - model_kwargs.update(settings.openai_kwargs()) - return ChatOpenAI(**model_kwargs) - - if settings.azure_api_key: - from langchain.chat_models import AzureChatOpenAI - - return AzureChatOpenAI(**model_kwargs) - - if settings.anthropic_api_key: - from langchain.chat_models import ChatAnthropic - - return ChatAnthropic(**model_kwargs) - - if settings.google_api_key: - from langchain.chat_models import ChatGooglePalm - - return ChatGooglePalm(**model_kwargs) - - raise ValueError( - "Could not read llm selector string. " - "Please check [here](https://github.com/shroominic/funcchain/blob/main/MODELS.md) for more info." - ) From 63b4e3d100f0f1c94a4f6a853640df00bc8c9acb Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:33:11 +0100 Subject: [PATCH 123/451] =?UTF-8?q?=F0=9F=94=A7=20mypy=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/py.typed diff --git a/src/funcchain/py.typed b/src/funcchain/py.typed new file mode 100644 index 0000000..e69de29 From 8fef41a213a70948175eb59c6c4c1a773495dee4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:34:38 +0100 Subject: [PATCH 124/451] =?UTF-8?q?=F0=9F=94=96=20literal=20str=20enum=20e?= =?UTF-8?q?xample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/literals.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/literals.py b/examples/literals.py index 2d0c449..b633c15 100644 --- a/examples/literals.py +++ b/examples/literals.py @@ -4,9 +4,11 @@ from pydantic import BaseModel +# just a silly example to schowcase the Literal type class Ranking(BaseModel): chain_of_thought: str score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] def rank_output(output: str) -> Ranking: From 2a045df14113731c111e0a847d589c63ec82c0c4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:34:55 +0100 Subject: [PATCH 125/451] =?UTF-8?q?=E2=9C=A8=20runnable=20decorator=20with?= =?UTF-8?q?=20rag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/decorator.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/decorator.py diff --git a/examples/decorator.py b/examples/decorator.py new file mode 100644 index 0000000..ce217f6 --- /dev/null +++ b/examples/decorator.py @@ -0,0 +1,33 @@ +from funcchain.syntax import chain, runnable +from langchain.embeddings.openai import OpenAIEmbeddings +from langchain_community.vectorstores.faiss import FAISS +from langchain_core.runnables import Runnable, RunnablePassthrough + + +@runnable +def generate_poem(topic: str, context: str) -> str: + """ + Generate a poem about the topic with the given context. + """ + return chain() + + +vectorstore = FAISS.from_texts( + [ + "japanese tea is full of heart warming flavors", + "in the morning you should take a walk", + "cold showers are good for your health", + ], + embedding=OpenAIEmbeddings(), +) +retriever = vectorstore.as_retriever() + +retrieval_chain: Runnable = { + "context": retriever, + "topic": RunnablePassthrough(), +} | generate_poem + + +result = retrieval_chain.invoke("love") + +print(result) From f86ddff21408b578d2830547d85e24732d488afe Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:22 +0100 Subject: [PATCH 126/451] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20update=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .python-version | 2 +- pyproject.toml | 5 ++++- requirements-dev.lock | 14 ++++++++------ requirements.lock | 26 +++++++++++--------------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.python-version b/.python-version index eb07499..375f5ca 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -cpython@3.12.0 +3.11.6 diff --git a/pyproject.toml b/pyproject.toml index b72a11c..43cdb06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ authors = [ { name = "Shroominic", email = "contact@shroominic.com" } ] dependencies = [ - "langchain[openai]>=0.0.352", + "langchain_core>=0.1.10", + "langchain_openai>=0.0.2.post1", "pydantic-settings>=2", "docstring-parser>=0.15", "rich>=13", @@ -42,10 +43,12 @@ dev-dependencies = [ "pytest", "ipython", "pre-commit", + "langchain>=0.1", "funcchain[all]", "mkdocs-material>=9.4", "beautifulsoup4>=4.12", "python-dotenv>=1", + "faiss-cpu>=1.7.4", ] [project.optional-dependencies] diff --git a/requirements-dev.lock b/requirements-dev.lock index 4347df8..589c9de 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -26,6 +26,7 @@ distlib==0.3.7 distro==1.8.0 docstring-parser==0.15 executing==2.0.1 +faiss-cpu==1.7.4 filelock==3.13.1 frozenlist==1.4.0 ghp-import==2.1.0 @@ -41,10 +42,11 @@ jedi==0.19.1 jinja2==3.1.2 jsonpatch==1.33 jsonpointer==2.4 -langchain==0.0.352 -langchain-community==0.0.6 -langchain-core==0.1.3 -langsmith==0.0.75 +langchain==0.1.0 +langchain-community==0.0.12 +langchain-core==0.1.10 +langchain-openai==0.0.2.post1 +langsmith==0.0.80 markdown==3.5.1 markdown-it-py==3.0.0 markupsafe==2.1.3 @@ -60,7 +62,7 @@ mypy==1.7.0 mypy-extensions==1.0.0 nodeenv==1.8.0 numpy==1.26.2 -openai==1.3.4 +openai==1.7.2 packaging==23.2 paginate==0.5.6 parso==0.8.3 @@ -93,7 +95,7 @@ soupsieve==2.5 sqlalchemy==2.0.23 stack-data==0.6.3 tenacity==8.2.3 -tiktoken==0.5.1 +tiktoken==0.5.2 tqdm==4.66.1 traitlets==5.14.0 typing-extensions==4.8.0 diff --git a/requirements.lock b/requirements.lock index 42df27e..14c4468 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,31 +7,27 @@ # all-features: false -e file:. -aiohttp==3.9.0 -aiosignal==1.3.1 annotated-types==0.6.0 anyio==3.7.1 -attrs==23.1.0 certifi==2023.11.17 charset-normalizer==3.3.2 -dataclasses-json==0.6.2 +distro==1.9.0 docstring-parser==0.15 -frozenlist==1.4.0 +h11==0.14.0 +httpcore==1.0.2 +httpx==0.26.0 idna==3.4 jinja2==3.1.2 jsonpatch==1.33 jsonpointer==2.4 -langchain==0.0.352 -langchain-community==0.0.6 -langchain-core==0.1.3 -langsmith==0.0.75 +langchain-core==0.1.10 +langchain-openai==0.0.2.post1 +langsmith==0.0.80 markdown-it-py==3.0.0 markupsafe==2.1.3 -marshmallow==3.20.1 mdurl==0.1.2 -multidict==6.0.4 -mypy-extensions==1.0.0 numpy==1.26.2 +openai==1.7.2 packaging==23.2 pillow==10.1.0 pydantic==2.5.2 @@ -40,12 +36,12 @@ pydantic-settings==2.1.0 pygments==2.17.2 python-dotenv==1.0.0 pyyaml==6.0.1 +regex==2023.12.25 requests==2.31.0 rich==13.7.0 sniffio==1.3.0 -sqlalchemy==2.0.23 tenacity==8.2.3 +tiktoken==0.5.2 +tqdm==4.66.1 typing-extensions==4.8.0 -typing-inspect==0.9.0 urllib3==2.1.0 -yarl==1.9.3 From 4f7d65352a9815ca6543418f752ba89b1fd4e37f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:37 +0100 Subject: [PATCH 127/451] mv --- src/funcchain/{utils/_llms.py => model/llm_overrides.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/funcchain/{utils/_llms.py => model/llm_overrides.py} (95%) diff --git a/src/funcchain/utils/_llms.py b/src/funcchain/model/llm_overrides.py similarity index 95% rename from src/funcchain/utils/_llms.py rename to src/funcchain/model/llm_overrides.py index e632774..6df2fe6 100644 --- a/src/funcchain/utils/_llms.py +++ b/src/funcchain/model/llm_overrides.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from langchain.chat_models import ChatOllama as _ChatOllama +from langchain_community.chat_models import ChatOllama as _ChatOllama from langchain_core.pydantic_v1 import validator From 835e768521fae7b1b0dea42e332fa97052f12ab1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 128/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20and=20?= =?UTF-8?q?method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/dynamic_router.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dynamic_router.py b/examples/dynamic_router.py index 41f4187..80a4ee8 100644 --- a/examples/dynamic_router.py +++ b/examples/dynamic_router.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, Callable, TypedDict -from funcchain import runnable +from funcchain.syntax.executable import compile_runnable from pydantic import BaseModel, Field # Dynamic Router Definition: @@ -31,7 +31,7 @@ class RouterModel(BaseModel): description="Enum of the available routes.", ) - route_query = runnable( + route_query = compile_runnable( instruction="Given the user query select the best query handler for it.", input_args=["user_query", "query_handlers"], output_type=RouterModel, From 2c8b0ebb6f10a9935e3f84210a0d13cf3ea4716a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 129/451] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20'runna?= =?UTF-8?q?ble'=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/__init__.py b/src/funcchain/__init__.py index 0b2df74..074884e 100644 --- a/src/funcchain/__init__.py +++ b/src/funcchain/__init__.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from .backend.settings import settings -from .syntax.executable import achain, chain, runnable +from .syntax.executable import achain, chain from .syntax.types import Error __all__ = [ From 07dbb904ea807a966902fc94d8d481db24a6e7b0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 130/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20compiler.py?= =?UTF-8?q?=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 34 +++++++++++++------------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index 470e59a..1d4a075 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -5,9 +5,8 @@ from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.language_models import BaseChatModel from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage -from langchain_core.output_parsers import BaseOutputParser -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.runnables import RunnableSerializable +from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser +from langchain_core.runnables import Runnable from PIL import Image from pydantic import BaseModel @@ -40,7 +39,7 @@ def create_union_chain( context: list[BaseMessage], llm: BaseChatModel, input_kwargs: dict[str, str], -) -> RunnableSerializable[dict[str, str], BaseModel]: +) -> Runnable[dict[str, str], BaseModel]: """ Compile a langchain runnable chain from the funcchain syntax. """ @@ -70,20 +69,17 @@ def create_union_chain( return prompt | llm | MultiToolParser(output_types=output_types) -# TODO: do patch instead of seperate creation -def create_pydanctic_chain( - output_type: type[BaseModel], - prompt: ChatPromptTemplate, +def parse_and_patch_pydanctic( llm: BaseChatModel, + output_type: type[BaseModel], input_kwargs: dict[str, str], -) -> RunnableSerializable[dict[str, str], BaseModel]: - # TODO: check these format_instructions +) -> BaseGenerationOutputParser: input_kwargs["format_instructions"] = f"Extract to {output_type.__name__}." functions = pydantic_to_functions(output_type) llm = llm.bind(**functions) # type: ignore - return prompt | llm | PydanticFuncParser(pydantic_schema=output_type) + return PydanticFuncParser(pydantic_schema=output_type) def create_chain( @@ -94,7 +90,7 @@ def create_chain( memory: BaseChatMessageHistory, settings: FuncchainSettings, input_kwargs: dict[str, str], -) -> RunnableSerializable[dict[str, str], ChainOutput]: +) -> Runnable[dict[str, str], ChainOutput]: """ Compile a langchain runnable chain from the funcchain syntax. """ @@ -109,6 +105,8 @@ def create_chain( if parser and (settings.streaming or not is_function_model(llm)): # streaming behavior is not supported for function models # but for normal function models we do not need to add format instructions + if not isinstance(parser, BaseOutputParser): + raise NotImplementedError("Fix this") instruction, f_instructions = _add_format_instructions( parser, instruction, @@ -157,17 +155,13 @@ def create_chain( if settings.streaming and hasattr(llm, "model_kwargs"): llm.model_kwargs = {"response_format": {"type": "json_object"}} else: - return create_pydanctic_chain( # type: ignore - output_type, - chat_prompt, - llm, - input_kwargs, - ) + parser = parse_and_patch_pydanctic(llm, output_type, input_kwargs) + assert parser is not None return chat_prompt | llm | parser -def compile_chain(signature: Signature[ChainOutput]) -> RunnableSerializable[dict[str, str], ChainOutput]: +def compile_chain(signature: Signature[ChainOutput]) -> Runnable[dict[str, str], ChainOutput]: """ Compile a signature to a runnable chain. """ @@ -258,7 +252,7 @@ def _inject_grammar_for_local_models(llm: BaseChatModel, output_type: type) -> N Inject GBNF grammar into local models. """ try: - from funcchain.utils._llms import ChatOllama + from funcchain.model.llm_overrides import ChatOllama except: # noqa pass else: From 76ef0de742bcf7171aab1884e4f441f65a219a4b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 131/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20settings=20attr?= =?UTF-8?q?ibutes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/settings.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/funcchain/backend/settings.py b/src/funcchain/backend/settings.py index 1b6e496..9c68a87 100644 --- a/src/funcchain/backend/settings.py +++ b/src/funcchain/backend/settings.py @@ -20,7 +20,7 @@ class FuncchainSettings(BaseSettings): console_stream: bool = False - default_system_prompt: str = "" + system_prompt: str = "" retry_parse: int = 3 retry_parse_sleep: float = 0.1 @@ -37,7 +37,7 @@ class FuncchainSettings(BaseSettings): max_tokens: int = 2048 temperature: float = 0.1 - # OLLAMA KWARGS + # LLAMACPP KWARGS context_lenght: int = 8196 n_gpu_layers: int = 50 keep_loaded: bool = False @@ -56,6 +56,9 @@ def openai_kwargs(self) -> dict: } def ollama_kwargs(self) -> dict: + return {} + + def llamacpp_kwargs(self) -> dict: return { "n_ctx": self.context_lenght, "use_mlock": self.keep_loaded, @@ -67,7 +70,7 @@ def ollama_kwargs(self) -> dict: class SettingsOverride(TypedDict, total=False): - llm: BaseChatModel | str + llm: BaseChatModel | str | None verbose: bool temperature: float @@ -75,6 +78,7 @@ class SettingsOverride(TypedDict, total=False): streaming: bool retry_parse: int context_lenght: int + system_prompt: str def create_local_settings(override: Optional[SettingsOverride] = None) -> FuncchainSettings: From f33a850606e37afe9e151cecbad999745192b98b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 132/451] =?UTF-8?q?=F0=9F=94=80=20Refactor=20import=20path?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/defaults.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/funcchain/model/defaults.py b/src/funcchain/model/defaults.py index de9f585..3857862 100644 --- a/src/funcchain/model/defaults.py +++ b/src/funcchain/model/defaults.py @@ -41,23 +41,23 @@ def univeral_model_selector( try: match mtype: case "openai": - from langchain.chat_models import ChatOpenAI + from langchain_openai.chat_models import ChatOpenAI model_kwargs.update(settings.openai_kwargs()) return ChatOpenAI(**model_kwargs) case "anthropic": - from langchain.chat_models import ChatAnthropic + from langchain_community.chat_models import ChatAnthropic return ChatAnthropic(**model_kwargs) case "google": - from langchain.chat_models import ChatGooglePalm + from langchain_community.chat_models import ChatGooglePalm return ChatGooglePalm(**model_kwargs) case "ollama": - from ..utils._llms import ChatOllama + from .llm_overrides import ChatOllama model = model_kwargs.pop("model_name") model_kwargs.update(settings.ollama_kwargs()) @@ -69,7 +69,7 @@ def univeral_model_selector( try: if "gpt-4" in name or "gpt-3.5" in name: - from langchain.chat_models import ChatOpenAI + from langchain_openai.chat_models import ChatOpenAI model_kwargs.update(settings.openai_kwargs()) return ChatOpenAI(**model_kwargs) @@ -80,23 +80,23 @@ def univeral_model_selector( model_kwargs.pop("model_name") if settings.openai_api_key: - from langchain.chat_models import ChatOpenAI + from langchain_openai.chat_models import ChatOpenAI model_kwargs.update(settings.openai_kwargs()) return ChatOpenAI(**model_kwargs) if settings.azure_api_key: - from langchain.chat_models import AzureChatOpenAI + from langchain_openai.chat_models import AzureChatOpenAI return AzureChatOpenAI(**model_kwargs) if settings.anthropic_api_key: - from langchain.chat_models import ChatAnthropic + from langchain_community.chat_models import ChatAnthropic return ChatAnthropic(**model_kwargs) if settings.google_api_key: - from langchain.chat_models import ChatGooglePalm + from langchain_community.chat_models import ChatGooglePalm return ChatGooglePalm(**model_kwargs) From dadd85f53895956c934efe860743e58ad1a37986 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 133/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20for=20?= =?UTF-8?q?retry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/parser/parsers.py b/src/funcchain/parser/parsers.py index fc20d42..e4ca874 100644 --- a/src/funcchain/parser/parsers.py +++ b/src/funcchain/parser/parsers.py @@ -241,9 +241,9 @@ def _type(self) -> str: @property def retry_chain(self) -> Runnable: - from ..syntax.executable import runnable + from ..syntax.executable import compile_runnable - return runnable( + return compile_runnable( instruction="Retry parsing the output by fixing the error.", input_args=["output", "error"], output_type=self.pydantic_object, From e745e75771ad6880d82a9dc50a6febec84f51eb4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 134/451] =?UTF-8?q?=E2=9C=A8=20Refactor=20signature=20sche?= =?UTF-8?q?ma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py index 3c5a351..7377ed0 100644 --- a/src/funcchain/schema/signature.py +++ b/src/funcchain/schema/signature.py @@ -1,7 +1,8 @@ from typing import Any, Generic, TypeVar from langchain.pydantic_v1 import BaseModel, Field -from langchain_core.language_models import BaseChatModel + +# from langchain_core.language_models import BaseChatModel from langchain_core.messages import BaseMessage from ..backend.settings import FuncchainSettings, settings @@ -20,12 +21,17 @@ class Signature(BaseModel, Generic[T]): input_args: list[str] = Field(default_factory=list) """ List of input arguments for the prompt template. """ - output_type: type[T] = Field(default=str) - """ Type to parse the output into. """ + # TODO collect types from input_args + # -> this would allow special prompt templating based on certain types + # -> e.g. BaseChatMessageHistory adds a history placeholder + # -> e.g. BaseChatModel overrides the default language model + # -> e.g. SettingsOverride overrides the default settings + # -> e.g. Callbacks adds custom callbacks + # -> e.g. SystemMessage adds a system message + # maybe do input_args: list[tuple[str, type]] = Field(default_factory=list) - # todo: should this be defined at compile time? maybe runtime is better - llm: BaseChatModel | str - """ Chat model to use as string or langchain object. """ + output_type: type[T] + """ Type to parse the output into. """ # todo: is history really needed? maybe this could be a background optimization history: list[BaseMessage] = Field(default_factory=list) @@ -33,9 +39,13 @@ class Signature(BaseModel, Generic[T]): # update_history: bool = Field(default=True) + # todo: should this be defined at compile time? maybe runtime is better settings: FuncchainSettings = Field(default=settings) """ Local settings to override global settings. """ + auto_tune: bool = Field(default=False) + """ Whether to auto tune the prompt using dspy. """ + class Config: arbitrary_types_allowed = True @@ -46,7 +56,6 @@ def __hash__(self) -> int: self.instruction, tuple(self.input_args), self.output_type, - self.llm, tuple(self.history), self.settings, ) From 2d4264ae1fe5bf65cd2fa780c979a6bcdb03fe4b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 135/451] =?UTF-8?q?=E2=9C=A8=20Add=20chaining=20utilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/funcchain/syntax/__init__.py b/src/funcchain/syntax/__init__.py index 0142f6e..3bafb1a 100644 --- a/src/funcchain/syntax/__init__.py +++ b/src/funcchain/syntax/__init__.py @@ -1 +1,10 @@ -# syntax -> signature +""" Syntax -> Signature +""" +from .decorators import runnable +from .executable import achain, chain + +__all__ = [ + "chain", + "achain", + "runnable", +] From faeacd741683d43663e7b6cbe0b19864fff7f547 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 136/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20and=20?= =?UTF-8?q?usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/router.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/funcchain/syntax/components/router.py b/src/funcchain/syntax/components/router.py index 35b5de7..57c8377 100644 --- a/src/funcchain/syntax/components/router.py +++ b/src/funcchain/syntax/components/router.py @@ -14,7 +14,7 @@ from typing_extensions import TypedDict from ...utils.msg_tools import msg_to_str -from ..executable import runnable +from ..executable import compile_runnable class Route(TypedDict): @@ -84,7 +84,7 @@ class RouterModel(BaseModel): description="Enum of the available routes.", ) - return runnable( + return compile_runnable( instruction="Given the user request select the appropriate route.", input_args=["user_request", "routes"], # todo: optional images output_type=RouterModel, @@ -97,7 +97,7 @@ def _add_default_handler(self) -> None: self.routes["default"] = { "handler": ( {"user_request": lambda x: msg_to_str(x)} - | runnable( + | compile_runnable( instruction="{user_request}", input_args=["user_request"], output_type=str, From 9673c4c52842cb2794ca39c8a80c4f1caa764070 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 137/451] =?UTF-8?q?=E2=9C=A8=20Add=20runnable=20decorator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/decorators.py | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/funcchain/syntax/decorators.py b/src/funcchain/syntax/decorators.py index e69de29..de3441c 100644 --- a/src/funcchain/syntax/decorators.py +++ b/src/funcchain/syntax/decorators.py @@ -0,0 +1,59 @@ +from types import FunctionType +from typing import Callable, Optional, TypeVar, Union, overload + +from langchain_core.language_models import BaseChatModel +from langchain_core.runnables import Runnable + +from ..backend.compiler import compile_chain +from ..backend.settings import SettingsOverride, create_local_settings +from ..schema.signature import Signature +from .meta_inspect import gather_signature + +OutputT = TypeVar("OutputT") + + +@overload +def runnable( + f: Callable[..., OutputT], +) -> Runnable[dict[str, str], OutputT]: + ... + + +@overload +def runnable( + *, + llm: BaseChatModel | str | None = None, + settings: SettingsOverride = {}, + auto_tune: bool = False, +) -> Callable[[Callable], Runnable[dict[str, str], OutputT]]: + ... + + +def runnable( + f: Optional[Callable[..., OutputT]] = None, + *, + llm: BaseChatModel | str | None = None, + settings: SettingsOverride = {}, + auto_tune: bool = False, +) -> Union[Callable, Runnable]: + """Decorator for funcchain syntax. + Compiles the function into a runnable. + """ + if llm: + settings["llm"] = llm + + def decorator(f: Callable) -> Runnable: + if not isinstance(f, FunctionType): + raise ValueError("funcchain can only be used on functions") + + _signature: dict = gather_signature(f) + _signature["settings"] = create_local_settings(override=settings) + _signature["auto_tune"] = auto_tune + + sig = Signature(**_signature) + return compile_chain(sig) + + if callable(f): + return decorator(f) + else: + return decorator From 4b9eb37e73b3f20b718e7fd0daa0b56a9f0b38ed Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 138/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20executable=20?= =?UTF-8?q?functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py index 1c1ea97..44ab616 100644 --- a/src/funcchain/syntax/executable.py +++ b/src/funcchain/syntax/executable.py @@ -37,14 +37,13 @@ def chain( memory = memory or ChatMessageHistory() input_kwargs.update(kwargs_from_parent()) - system = system or settings.default_system_prompt + system = system or settings.system_prompt instruction = instruction or from_docstring() sig: Signature = Signature( instruction=instruction, input_args=list(input_kwargs.keys()), output_type=output_type, - llm=settings_override.get("llm", settings.llm), history=context, settings=settings, ) @@ -75,14 +74,15 @@ async def achain( memory = memory or ChatMessageHistory() input_kwargs.update(kwargs_from_parent()) - system = system or settings.default_system_prompt + + # todo maybe this should be done in the prompt processor? + system = system or settings.system_prompt instruction = instruction or from_docstring() sig: Signature = Signature( instruction=instruction, input_args=list(input_kwargs.keys()), output_type=output_type, - llm=settings_override.get("llm", settings.llm), history=context, settings=settings, ) @@ -96,7 +96,7 @@ async def achain( return result -def runnable( +def compile_runnable( instruction: str, output_type: type[ChainOut], input_args: list[str] = [], @@ -106,7 +106,7 @@ def runnable( settings_override: SettingsOverride = {}, ) -> Runnable[dict[str, str], ChainOut]: """ - Experimental replacement for using the funcchain syntax. + On the fly compilation of the funcchain syntax. """ if settings_override and llm: settings_override["llm"] = llm @@ -118,7 +118,6 @@ def runnable( instruction=instruction, input_args=input_args, output_type=output_type, - llm=settings_override.get("llm", settings.llm), history=context, settings=settings, ) From d5e7df5a3328ee87a5da7c3ac51f1e99fd64c6ba Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 139/451] =?UTF-8?q?=E2=9C=A8=20Enhance=20meta=20inspection?= =?UTF-8?q?=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/meta_inspect.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/funcchain/syntax/meta_inspect.py b/src/funcchain/syntax/meta_inspect.py index e575437..d0fff3d 100644 --- a/src/funcchain/syntax/meta_inspect.py +++ b/src/funcchain/syntax/meta_inspect.py @@ -1,9 +1,9 @@ import types from inspect import FrameInfo, currentframe, getouterframes -from typing import Union +from typing import Any, Optional, Union from langchain_core.language_models import BaseChatModel -from langchain_core.output_parsers import BaseOutputParser, StrOutputParser +from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser, StrOutputParser from ..parser.parsers import ( BoolOutputParser, @@ -38,22 +38,21 @@ def get_func_obj() -> types.FunctionType: return func -def from_docstring() -> str: +def from_docstring(f: Optional[types.FunctionType] = None) -> str: """ Get the docstring of the parent caller function. """ - if doc_str := get_func_obj().__doc__: + if doc_str := (f or get_func_obj()).__doc__: return "\n".join([line.lstrip() for line in doc_str.split("\n")]) raise ValueError(f"The funcchain ({get_parent_frame().function}) must have a docstring") -def get_output_type() -> type: +def get_output_type(f: Optional[types.FunctionType] = None) -> type: """ Get the output type annotation of the parent caller function. """ try: - # print(get_parent_frame().frame.f_globals) - return get_func_obj().__annotations__["return"] + return (f or get_func_obj()).__annotations__["return"] except KeyError: raise ValueError("The funcchain must have a return type annotation") @@ -62,7 +61,7 @@ def parser_for( output_type: type, retry: int, llm: BaseChatModel | str | None = None, -) -> BaseOutputParser | None: +) -> BaseOutputParser | BaseGenerationOutputParser: """ Get the parser from the type annotation of the parent caller function. """ @@ -85,7 +84,7 @@ def parser_for( if issubclass(output_type, BaseModel): return RetryPydanticOutputParser(pydantic_object=output_type, retry=retry, retry_llm=llm) else: - raise RuntimeError(f"Output Type is not supported: {output_type}") + raise SyntaxError(f"Output Type is not supported: {output_type}") def kwargs_from_parent() -> dict[str, str]: @@ -93,3 +92,14 @@ def kwargs_from_parent() -> dict[str, str]: Get the kwargs from the parent function. """ return get_parent_frame(FUNC_DEPTH - 1).frame.f_locals + + +def gather_signature(f: types.FunctionType) -> dict[str, Any]: + """ + Gather the signature of the parent caller function. + """ + return { + "instruction": from_docstring(f), + "input_args": list(f.__code__.co_varnames[: f.__code__.co_argcount]), + "output_type": get_output_type(f), + } From eb98f4cd178b7776bcba534ba3424e602738075c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 15:35:53 +0100 Subject: [PATCH 140/451] =?UTF-8?q?=F0=9F=94=84=20Update=20output=20parser?= =?UTF-8?q?=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/syntax/types.py b/src/funcchain/syntax/types.py index 5683ebf..cd71a53 100644 --- a/src/funcchain/syntax/types.py +++ b/src/funcchain/syntax/types.py @@ -3,14 +3,14 @@ from typing import Optional from langchain_core.exceptions import OutputParserException -from langchain_core.output_parsers import BaseOutputParser +from langchain_core.output_parsers import BaseLLMOutputParser from pydantic import BaseModel, Field from typing_extensions import Self class ParserBaseModel(BaseModel): @classmethod - def output_parser(cls) -> BaseOutputParser[Self]: + def output_parser(cls) -> BaseLLMOutputParser[Self]: from ..parser.parsers import CustomPydanticOutputParser return CustomPydanticOutputParser(pydantic_object=cls) From c010e6cfc31bfc31e74cf4397b56840b25fb9bd4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 141/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/dynamic_model_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/experiments/dynamic_model_generation.py b/examples/experiments/dynamic_model_generation.py index ee1dff6..21b3de3 100644 --- a/examples/experiments/dynamic_model_generation.py +++ b/examples/experiments/dynamic_model_generation.py @@ -1,5 +1,5 @@ from funcchain import chain, settings -from funcchain.syntax.types import CodeBlock +from funcchain.syntax.output_types import CodeBlock from langchain.document_loaders import WebBaseLoader from pydantic import BaseModel from rich import print From 5da0b732613f82f53d258b3ee81d44bda01a0e37 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 142/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/local_codeblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/experiments/local_codeblock.py b/examples/experiments/local_codeblock.py index 964c450..03eadc7 100644 --- a/examples/experiments/local_codeblock.py +++ b/examples/experiments/local_codeblock.py @@ -1,5 +1,5 @@ from funcchain import chain, settings -from funcchain.syntax.types import CodeBlock +from funcchain.syntax.output_types import CodeBlock def generate_code(instruction: str) -> CodeBlock: From bd01a2a2ec9c74d6aaded3ad9e5cb8334c0986ee Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 143/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/parallel_console_streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/experiments/parallel_console_streaming.py b/examples/experiments/parallel_console_streaming.py index 8973316..693ee65 100644 --- a/examples/experiments/parallel_console_streaming.py +++ b/examples/experiments/parallel_console_streaming.py @@ -5,7 +5,7 @@ from funcchain import achain, settings from funcchain.backend.streaming import astream_to -from funcchain.utils.funcs import count_tokens +from funcchain.utils.token_counter import count_tokens from rich.console import Console from rich.layout import Layout from rich.live import Live From 792f78bd8ae6ab63d1d6724adddd416c4258feba Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 144/451] =?UTF-8?q?=E2=9C=A8=20Add=20PyYAML=20type=20annot?= =?UTF-8?q?ations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 43cdb06..e1e2810 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dev-dependencies = [ "pre-commit", "langchain>=0.1", "funcchain[all]", + "types-PyYAML>=6", "mkdocs-material>=9.4", "beautifulsoup4>=4.12", "python-dotenv>=1", From 7ec9a39239731620607c8b5b72ec9749df6e79c8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 145/451] =?UTF-8?q?=F0=9F=94=80=20Update=20Error=20import?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/__init__.py b/src/funcchain/__init__.py index 074884e..ac08f48 100644 --- a/src/funcchain/__init__.py +++ b/src/funcchain/__init__.py @@ -2,7 +2,7 @@ from .backend.settings import settings from .syntax.executable import achain, chain -from .syntax.types import Error +from .syntax.output_types import Error __all__ = [ "settings", From f7d6f215770184a05509f5f1b24eaaeca7904f67 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 146/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20compiler.py?= =?UTF-8?q?=20parsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index 1d4a075..cdfcb2e 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -10,15 +10,16 @@ from PIL import Image from pydantic import BaseModel -from ..model.abilities import is_function_model, is_vision_model +from ..model.abilities import is_openai_function_model, is_vision_model from ..model.defaults import univeral_model_selector -from ..parser.grammars import pydantic_to_grammar -from ..parser.parsers import MultiToolParser, ParserBaseModel, PydanticFuncParser +from ..parser.openai_functions import OpenAIFunctionPydanticParser, OpenAIFunctionPydanticUnionParser +from ..parser.schema_converter import pydantic_to_grammar +from ..parser.selector import parser_for from ..schema.signature import Signature -from ..syntax.meta_inspect import parser_for -from ..utils.funcs import count_tokens +from ..syntax.output_types import ParserBaseModel from ..utils.msg_tools import msg_to_str from ..utils.pydantic import multi_pydantic_to_functions, pydantic_to_functions +from ..utils.token_counter import count_tokens from .prompt import ( HumanImageMessagePromptTemplate, create_chat_prompt, @@ -66,10 +67,10 @@ def create_union_chain( memory=memory, ) - return prompt | llm | MultiToolParser(output_types=output_types) + return prompt | llm | OpenAIFunctionPydanticUnionParser(output_types=output_types) -def parse_and_patch_pydanctic( +def parse_function_to_pydantic( llm: BaseChatModel, output_type: type[BaseModel], input_kwargs: dict[str, str], @@ -79,7 +80,7 @@ def parse_and_patch_pydanctic( llm = llm.bind(**functions) # type: ignore - return PydanticFuncParser(pydantic_schema=output_type) + return OpenAIFunctionPydanticParser(pydantic_schema=output_type) def create_chain( @@ -102,7 +103,7 @@ def create_chain( # add format instructions for parser f_instructions = None - if parser and (settings.streaming or not is_function_model(llm)): + if parser and (settings.streaming or not is_openai_function_model(llm)): # streaming behavior is not supported for function models # but for normal function models we do not need to add format instructions if not isinstance(parser, BaseOutputParser): @@ -139,7 +140,7 @@ def create_chain( _inject_grammar_for_local_models(llm, output_type) # function model patches - if is_function_model(llm): + if is_openai_function_model(llm): if isinstance(output_type, UnionType): return create_union_chain( output_type, @@ -155,7 +156,7 @@ def create_chain( if settings.streaming and hasattr(llm, "model_kwargs"): llm.model_kwargs = {"response_format": {"type": "json_object"}} else: - parser = parse_and_patch_pydanctic(llm, output_type, input_kwargs) + parser = parse_function_to_pydantic(llm, output_type, input_kwargs) assert parser is not None return chat_prompt | llm | parser From 81cac02bd94868e7737eaab071d08b518470f0c8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 147/451] =?UTF-8?q?=F0=9F=94=80=20Renamed=20meta=5Finspect?= =?UTF-8?q?.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/meta_inspect.py | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/funcchain/backend/meta_inspect.py diff --git a/src/funcchain/backend/meta_inspect.py b/src/funcchain/backend/meta_inspect.py new file mode 100644 index 0000000..cd38b86 --- /dev/null +++ b/src/funcchain/backend/meta_inspect.py @@ -0,0 +1,66 @@ +import types +from inspect import FrameInfo, currentframe, getouterframes +from typing import Any, Optional + +FUNC_DEPTH = 4 + + +def get_parent_frame(depth: int = FUNC_DEPTH) -> FrameInfo: + """ + Get the dep'th parent function information. + """ + return getouterframes(currentframe())[depth] + + +def get_func_obj() -> types.FunctionType: + """ + Get the parent caller function. + """ + func_name = get_parent_frame().function + if func_name == "": + raise RuntimeError("Cannot get function object from module") + if func_name == "": + raise RuntimeError("Cannot get function object from lambda") + + try: + func = get_parent_frame().frame.f_globals[func_name] + except KeyError: + func = get_parent_frame(FUNC_DEPTH + 1).frame.f_locals[func_name] + return func + + +def from_docstring(f: Optional[types.FunctionType] = None) -> str: + """ + Get the docstring of the parent caller function. + """ + if doc_str := (f or get_func_obj()).__doc__: + return "\n".join([line.lstrip() for line in doc_str.split("\n")]) + raise ValueError(f"The funcchain ({get_parent_frame().function}) must have a docstring") + + +def get_output_type(f: Optional[types.FunctionType] = None) -> type: + """ + Get the output type annotation of the parent caller function. + """ + try: + return (f or get_func_obj()).__annotations__["return"] + except KeyError: + raise ValueError("The funcchain must have a return type annotation") + + +def kwargs_from_parent() -> dict[str, str]: + """ + Get the kwargs from the parent function. + """ + return get_parent_frame(FUNC_DEPTH - 1).frame.f_locals + + +def gather_signature(f: types.FunctionType) -> dict[str, Any]: + """ + Gather the signature of the parent caller function. + """ + return { + "instruction": from_docstring(f), + "input_args": list(f.__code__.co_varnames[: f.__code__.co_argcount]), + "output_type": get_output_type(f), + } From 5b45bd3d531673f41fe1b119aed2d4350aae1299 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 148/451] =?UTF-8?q?=F0=9F=94=80=20Refactor=20import,=20ren?= =?UTF-8?q?ame=20func?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/abilities.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/funcchain/model/abilities.py b/src/funcchain/model/abilities.py index 70ec683..a808879 100644 --- a/src/funcchain/model/abilities.py +++ b/src/funcchain/model/abilities.py @@ -1,4 +1,3 @@ -from langchain.chat_models import ChatOllama, ChatOpenAI from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage @@ -26,6 +25,8 @@ def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: + from langchain_community.chat_models import ChatOllama, ChatOpenAI # TODO: fix make this optional + if not isinstance(llm, BaseChatModel): return "base_model" if isinstance(llm, ChatOpenAI): @@ -66,7 +67,7 @@ def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: return "chat_model" -def is_function_model( +def is_openai_function_model( llm: BaseChatModel, ) -> bool: return gather_llm_type(llm) == "function_model" From 96a1364ebf619197f5f8f9cb31aecd6db3dc805b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 149/451] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20Pydantic=20p?= =?UTF-8?q?arser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/custom.py | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/funcchain/parser/custom.py b/src/funcchain/parser/custom.py index e69de29..dd79a44 100644 --- a/src/funcchain/parser/custom.py +++ b/src/funcchain/parser/custom.py @@ -0,0 +1,39 @@ +import json +from typing import Type, TypeVar + +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers import BaseOutputParser +from pydantic import ValidationError + +from ..syntax.output_types import CodeBlock as CodeBlock +from ..syntax.output_types import ParserBaseModel + +P = TypeVar("P", bound=ParserBaseModel) + + +class CustomPydanticOutputParser(BaseOutputParser[P]): + pydantic_object: Type[P] + + def parse(self, text: str) -> P: + try: + return self.pydantic_object.parse(text) + except (json.JSONDecodeError, ValidationError) as e: + raise OutputParserException( + f"Failed to parse {self.pydantic_object.__name__} " f"from completion {text}. Got: {e}", + llm_output=text, + ) + + def get_format_instructions(self) -> str: + reduced_schema = self.pydantic_object.model_json_schema() + if "title" in reduced_schema: + del reduced_schema["title"] + if "type" in reduced_schema: + del reduced_schema["type"] + + return self.pydantic_object.format_instructions().format( + schema=json.dumps(reduced_schema), + ) + + @property + def _type(self) -> str: + return "pydantic" From e53aef9d43af174caa3fb23eda7942f6fcd50b5c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 150/451] =?UTF-8?q?=E2=9C=A8=20Add=20JSON=20schema=20parse?= =?UTF-8?q?rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/json_schema.py | 90 +++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/funcchain/parser/json_schema.py diff --git a/src/funcchain/parser/json_schema.py b/src/funcchain/parser/json_schema.py new file mode 100644 index 0000000..3da00c1 --- /dev/null +++ b/src/funcchain/parser/json_schema.py @@ -0,0 +1,90 @@ +import json +import re +from typing import Type, TypeVar + +import yaml +from langchain_core.exceptions import OutputParserException +from langchain_core.language_models import BaseChatModel +from langchain_core.output_parsers import BaseOutputParser +from langchain_core.runnables import Runnable +from pydantic import BaseModel, ValidationError + +from ..syntax.output_types import CodeBlock as CodeBlock + +M = TypeVar("M", bound=BaseModel) + + +class RetryJsonPydanticParser(BaseOutputParser[M]): + """Parse an output using a pydantic model.""" + + pydantic_object: Type[M] + """The pydantic model to parse.""" + + retry: int + retry_llm: BaseChatModel | str | None = None + + def parse(self, text: str) -> M: + try: + matches = re.findall(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) + if len(matches) > 1: + for match in matches: + try: + json_object = json.loads(match, strict=False) + return self.pydantic_object.model_validate(json_object) + except (json.JSONDecodeError, ValidationError): + continue + elif len(matches) == 1: + json_object = json.loads(matches[0], strict=False) + return self.pydantic_object.model_validate(json_object) + # no matches + raise OutputParserException( + f"No JSON {self.pydantic_object.__name__} found in completion {text}.", + llm_output=text, + ) + except (json.JSONDecodeError, ValidationError) as e: + if self.retry > 0: + print(f"Retrying parsing {self.pydantic_object.__name__}...") + return self.retry_chain.invoke( + input={"output": text, "error": str(e)}, + config={"run_name": "RetryPydanticOutputParser"}, + ) + # no retries left + raise OutputParserException(str(e), llm_output=text) + + def get_format_instructions(self) -> str: + schema = self.pydantic_object.model_json_schema() + + # Remove extraneous fields. + reduced_schema = schema + if "title" in reduced_schema: + del reduced_schema["title"] + if "type" in reduced_schema: + del reduced_schema["type"] + # Ensure json in context is well-formed with double quotes. + schema_str = yaml.dump(reduced_schema) + + return ( + "Please respond with a json result matching the following schema:" + f"\n\n```schema\n{schema_str}\n```\n" + "Do not repeat the schema. Only respond with the result." + ) + + @property + def _type(self) -> str: + return "pydantic" + + @property + def retry_chain(self) -> Runnable: + from ..syntax.executable import compile_runnable + + return compile_runnable( + instruction="Retry parsing the output by fixing the error.", + input_args=["output", "error"], + output_type=self.pydantic_object, + llm=self.retry_llm, + settings_override={"retry_parse": self.retry - 1}, + ) + + +class RetryJsonPydanticUnionParser(BaseOutputParser[M]): + ... From 00d861340a2f14af27f249a4dad9e29ff13e59ff Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 151/451] =?UTF-8?q?=E2=9C=A8=20Add=20OpenAI=20parsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/openai_functions.py | 117 +++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/funcchain/parser/openai_functions.py diff --git a/src/funcchain/parser/openai_functions.py b/src/funcchain/parser/openai_functions.py new file mode 100644 index 0000000..910eaba --- /dev/null +++ b/src/funcchain/parser/openai_functions.py @@ -0,0 +1,117 @@ +import copy +from typing import Type, TypeVar + +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers import BaseGenerationOutputParser +from langchain_core.outputs import ChatGeneration, Generation +from pydantic import BaseModel, ValidationError + +from ..syntax.output_types import CodeBlock as CodeBlock +from ..utils.msg_tools import msg_to_str + +M = TypeVar("M", bound=BaseModel) + + +# TODO: retry wrapper +class OpenAIFunctionPydanticParser(BaseGenerationOutputParser[M]): + pydantic_schema: Type[M] + args_only: bool = False + + def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException( + "This output parser can only be used with a chat generation.", + ) + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + + if self.args_only: + _result = func_call["arguments"] + else: + _result = func_call + try: + if self.args_only: + pydantic_args = self.pydantic_schema.model_validate_json(_result) + else: + pydantic_args = self.pydantic_schema.model_validate_json(_result["arguments"]) + except ValidationError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + return pydantic_args + + +# TODO: retry wrapper +class OpenAIFunctionPydanticUnionParser(BaseGenerationOutputParser[M]): + output_types: list[Type[M]] + args_only: bool = False + + def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: + function_call = self._pre_parse_function_call(result) + + output_type_names = [t.__name__.lower() for t in self.output_types] + + if function_call["name"] not in output_type_names: + raise OutputParserException("Invalid function call") + + print(function_call["name"]) + + output_type = self._get_output_type(function_call["name"]) + + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException("This output parser can only be used with a chat generation.") + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + + if self.args_only: + _result = func_call["arguments"] + else: + _result = func_call + + try: + if self.args_only: + pydantic_args = output_type.model_validate_json(_result) + else: + pydantic_args = output_type.model_validate_json(_result["arguments"]) + except ValidationError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) + return pydantic_args + + def _pre_parse_function_call(self, result: list[Generation]) -> dict: + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException("This output parser can only be used with a chat generation.") + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError: + raise OutputParserException( + "The model refused to respond with a " f"function call:\n{message.content}\n\n", + llm_output=msg_to_str(message), + ) + + return func_call + + def _get_output_type(self, function_name: str) -> Type[M]: + output_type_iter = filter(lambda t: t.__name__.lower() == function_name, self.output_types) + if output_type_iter is None: + raise OutputParserException(f"No parser found for function: {function_name}") + return next(output_type_iter) From 4a95dcb20d25d77d01b9b90778e63f8ba01ca6b6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 152/451] =?UTF-8?q?=F0=9F=94=A5=20Remove=20deprecated=20pa?= =?UTF-8?q?rsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/parsers.py | 240 +------------------------------- 1 file changed, 4 insertions(+), 236 deletions(-) diff --git a/src/funcchain/parser/parsers.py b/src/funcchain/parser/parsers.py index e4ca874..68bcf3f 100644 --- a/src/funcchain/parser/parsers.py +++ b/src/funcchain/parser/parsers.py @@ -1,36 +1,13 @@ -import copy -import json -import re -from typing import Callable, Optional, Type, TypeVar +from typing import TypeVar -import yaml # type: ignore -from langchain_core.exceptions import OutputParserException -from langchain_core.language_models import BaseChatModel -from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser -from langchain_core.outputs import ChatGeneration, Generation -from langchain_core.runnables import Runnable -from pydantic import BaseModel, ValidationError +from langchain_core.output_parsers import BaseOutputParser -from ..syntax.types import CodeBlock as CodeBlock -from ..syntax.types import ParserBaseModel -from ..utils.msg_tools import msg_to_str +from ..syntax.output_types import CodeBlock as CodeBlock T = TypeVar("T") -class LambdaOutputParser(BaseOutputParser[T]): - _parse: Optional[Callable[[str], T]] = None - - def parse(self, text: str) -> T: - if self._parse is None: - raise NotImplementedError("LambdaOutputParser.lambda_parse() is not implemented") - return self._parse(text) - - @property - def _type(self) -> str: - return "lambda" - - +# TODO: remove and implement primitive type output parser using nested pydantic extraction class BoolOutputParser(BaseOutputParser[bool]): def parse(self, text: str) -> bool: return text.strip()[:1].lower() == "y" @@ -41,212 +18,3 @@ def get_format_instructions(self) -> str: @property def _type(self) -> str: return "bool" - - -M = TypeVar("M", bound=BaseModel) - - -class PydanticFuncParser(BaseGenerationOutputParser[M]): - pydantic_schema: Type[M] - args_only: bool = False - - def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation.", - ) - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) - - if self.args_only: - _result = func_call["arguments"] - else: - _result = func_call - try: - if self.args_only: - pydantic_args = self.pydantic_schema.model_validate_json(_result) - else: - pydantic_args = self.pydantic_schema.model_validate_json(_result["arguments"]) - except ValidationError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) - return pydantic_args - - -class MultiToolParser(BaseGenerationOutputParser[M]): - output_types: list[Type[M]] - args_only: bool = False - - def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: - function_call = self._pre_parse_function_call(result) - - output_type_names = [t.__name__.lower() for t in self.output_types] - - if function_call["name"] not in output_type_names: - raise OutputParserException("Invalid function call") - - print(function_call["name"]) - - output_type = self._get_output_type(function_call["name"]) - - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException("This output parser can only be used with a chat generation.") - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) - - if self.args_only: - _result = func_call["arguments"] - else: - _result = func_call - - try: - if self.args_only: - pydantic_args = output_type.model_validate_json(_result) - else: - pydantic_args = output_type.model_validate_json(_result["arguments"]) - except ValidationError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) - return pydantic_args - - def _pre_parse_function_call(self, result: list[Generation]) -> dict: - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException("This output parser can only be used with a chat generation.") - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError: - raise OutputParserException( - "The model refused to respond with a " f"function call:\n{message.content}\n\n", - llm_output=msg_to_str(message), - ) - - return func_call - - def _get_output_type(self, function_name: str) -> Type[M]: - output_type_iter = filter(lambda t: t.__name__.lower() == function_name, self.output_types) - if output_type_iter is None: - raise OutputParserException(f"No parser found for function: {function_name}") - return next(output_type_iter) - - -P = TypeVar("P", bound=ParserBaseModel) - - -class CustomPydanticOutputParser(BaseOutputParser[P]): - pydantic_object: Type[P] - - def parse(self, text: str) -> P: - try: - return self.pydantic_object.parse(text) - except (json.JSONDecodeError, ValidationError) as e: - raise OutputParserException( - f"Failed to parse {self.pydantic_object.__name__} " f"from completion {text}. Got: {e}", - llm_output=text, - ) - - def get_format_instructions(self) -> str: - reduced_schema = self.pydantic_object.model_json_schema() - if "title" in reduced_schema: - del reduced_schema["title"] - if "type" in reduced_schema: - del reduced_schema["type"] - - return self.pydantic_object.format_instructions().format( - schema=json.dumps(reduced_schema), - ) - - @property - def _type(self) -> str: - return "pydantic" - - -class RetryPydanticOutputParser(BaseOutputParser[M]): - """Parse an output using a pydantic model.""" - - pydantic_object: Type[M] - """The pydantic model to parse.""" - - retry: int - retry_llm: BaseChatModel | str | None = None - - def parse(self, text: str) -> M: - try: - matches = re.findall(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) - if len(matches) > 1: - for match in matches: - try: - json_object = json.loads(match, strict=False) - return self.pydantic_object.model_validate(json_object) - except (json.JSONDecodeError, ValidationError): - continue - elif len(matches) == 1: - json_object = json.loads(matches[0], strict=False) - return self.pydantic_object.model_validate(json_object) - # no matches - raise OutputParserException( - f"No JSON {self.pydantic_object.__name__} found in completion {text}.", - llm_output=text, - ) - except (json.JSONDecodeError, ValidationError) as e: - if self.retry > 0: - print(f"Retrying parsing {self.pydantic_object.__name__}...") - return self.retry_chain.invoke( - input={"output": text, "error": str(e)}, - config={"run_name": "RetryPydanticOutputParser"}, - ) - # no retries left - raise OutputParserException(str(e), llm_output=text) - - def get_format_instructions(self) -> str: - schema = self.pydantic_object.model_json_schema() - - # Remove extraneous fields. - reduced_schema = schema - if "title" in reduced_schema: - del reduced_schema["title"] - if "type" in reduced_schema: - del reduced_schema["type"] - # Ensure json in context is well-formed with double quotes. - schema_str = yaml.dump(reduced_schema) - - return ( - "Please respond with a json result matching the following schema:" - f"\n\n```schema\n{schema_str}\n```\n" - "Do not repeat the schema. Only respond with the result." - ) - - @property - def _type(self) -> str: - return "pydantic" - - @property - def retry_chain(self) -> Runnable: - from ..syntax.executable import compile_runnable - - return compile_runnable( - instruction="Retry parsing the output by fixing the error.", - input_args=["output", "error"], - output_type=self.pydantic_object, - llm=self.retry_llm, - settings_override={"retry_parse": self.retry - 1}, - ) From a29c97ed8aaa1dab6f31c3626cf399d2aaf30431 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 153/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20pydan?= =?UTF-8?q?tic.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/pydantic.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/funcchain/parser/pydantic.py diff --git a/src/funcchain/parser/pydantic.py b/src/funcchain/parser/pydantic.py deleted file mode 100644 index 8b13789..0000000 --- a/src/funcchain/parser/pydantic.py +++ /dev/null @@ -1 +0,0 @@ - From 6aadb0c49ce9f84fe732a6409cc1a58d9f740a1f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 154/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20schema=5Fconver?= =?UTF-8?q?ter=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/schema_converter.py | 130 +++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/funcchain/parser/schema_converter.py diff --git a/src/funcchain/parser/schema_converter.py b/src/funcchain/parser/schema_converter.py new file mode 100644 index 0000000..47d785d --- /dev/null +++ b/src/funcchain/parser/schema_converter.py @@ -0,0 +1,130 @@ +import json +import re +from typing import Type + +from pydantic import BaseModel + +SPACE_RULE = '" "?' + +PRIMITIVE_RULES = { + "boolean": '("true" | "false") space', + "number": '("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space', + "integer": '("-"? ([0-9] | [1-9] [0-9]*)) space', + "string": r""" "\"" ( + [^"\\] | + "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) + )* "\"" space """, + "null": '"null" space', +} + +INVALID_RULE_CHARS_RE = re.compile(r"[^a-zA-Z0-9-]+") +GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"]') +GRAMMAR_LITERAL_ESCAPES = {"\r": "\\r", "\n": "\\n", '"': '\\"'} + + +class SchemaConverter: + def __init__(self, prop_order: dict, defs: dict) -> None: + self._prop_order = prop_order + self._defs = defs + self._rules = {"space": SPACE_RULE} + + def _format_literal(self, literal: str) -> str: + escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub( + lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)), # type: ignore + json.dumps(literal), + ) + return f'"{escaped}"' + + def _add_rule(self, name: str, rule: str) -> str: + esc_name = INVALID_RULE_CHARS_RE.sub("-", name) + if esc_name not in self._rules or self._rules[esc_name] == rule: + key = esc_name + else: + i = 0 + while f"{esc_name}{i}" in self._rules: + i += 1 + key = f"{esc_name}{i}" + self._rules[key] = rule + return key + + def visit(self, schema: dict, name: str) -> str: + schema_type = schema.get("type") + rule_name = name or "root" + + if "$ref" in schema: + ref_name = schema["$ref"].split("/")[-1] + assert ref_name in self._defs, f"Unresolved reference: {schema['$ref']}" + return self.visit(self._defs[ref_name], ref_name) + + elif "oneOf" in schema or "anyOf" in schema: + rule = " | ".join( + ( + self.visit(alt_schema, f'{name}{"-" if name else ""}{i}') + for i, alt_schema in enumerate(schema.get("oneOf") or schema["anyOf"]) + ) + ) + return self._add_rule(rule_name, rule) + + elif "allOf" in schema: + rule = " ".join( + ( + self.visit(sub_schema, f'{name}{"-" if name else ""}{i}') + for i, sub_schema in enumerate(schema["allOf"]) + ) + ) + return self._add_rule(rule_name, rule) + + elif "const" in schema: + return self._add_rule(rule_name, self._format_literal(schema["const"])) + + elif "enum" in schema: + rule = " | ".join((self._format_literal(v) for v in schema["enum"])) + return self._add_rule(rule_name, rule) + + elif schema_type == "object" and "properties" in schema: + # TODO: `required` keyword + prop_order = self._prop_order + prop_pairs = sorted( + schema["properties"].items(), + # sort by position in prop_order (if specified) then by key + key=lambda kv: (prop_order.get(kv[0], len(prop_order)), kv[0]), + ) + + rule = '"{" space' + for i, (prop_name, prop_schema) in enumerate(prop_pairs): + prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}') + if i > 0: + rule += ' "," space' + rule += rf' {self._format_literal(prop_name)} space ":" space {prop_rule_name}' + rule += ' "}" space' + + return self._add_rule(rule_name, rule) + + elif schema_type == "array" and "items" in schema: + # TODO `prefixItems` keyword + item_rule_name = self.visit(schema["items"], f'{name}{"-" if name else ""}item') + rule = f'"[" space ({item_rule_name} ("," space {item_rule_name})*)? "]" space' + return self._add_rule(rule_name, rule) + + else: + assert schema_type in PRIMITIVE_RULES, f"Unrecognized schema: {schema}" + return self._add_rule( + "root" if rule_name == "root" else schema_type, + PRIMITIVE_RULES[schema_type], + ) + + def format_grammar(self) -> str: + return "\n".join((f"{name} ::= {rule}" for name, rule in self._rules.items())) + + +def schema_to_grammar(json_schema: dict) -> str: + schema = json_schema + prop_order = {name: idx for idx, name in enumerate(schema["properties"].keys())} + defs = schema.get("$defs", {}) + converter = SchemaConverter(prop_order, defs) + converter.visit(schema, "") + return converter.format_grammar() + + +def pydantic_to_grammar(model: Type[BaseModel]) -> str: + return schema_to_grammar(model.model_json_schema()) From c4add99da084ef088933044df792dfeb6a21f65e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 155/451] =?UTF-8?q?=E2=9C=A8=20Add=20dynamic=20parser=20se?= =?UTF-8?q?lection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/selector.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/funcchain/parser/selector.py b/src/funcchain/parser/selector.py index e69de29..e72dd85 100644 --- a/src/funcchain/parser/selector.py +++ b/src/funcchain/parser/selector.py @@ -0,0 +1,35 @@ +import types +from typing import Union + +from langchain_core.language_models import BaseChatModel +from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser, StrOutputParser + +from ..parser.json_schema import RetryJsonPydanticParser, RetryJsonPydanticUnionParser +from ..parser.parsers import BoolOutputParser +from ..syntax.output_types import ParserBaseModel + + +def parser_for( + output_type: type, + retry: int, + llm: BaseChatModel | str | None = None, +) -> BaseOutputParser | BaseGenerationOutputParser: + """ + Get the parser from the type annotation of the parent caller function. + """ + if isinstance(output_type, types.UnionType) or getattr(output_type, "__origin__", None) is Union: + output_type = output_type.__args__[0] # type: ignore + return RetryJsonPydanticUnionParser(pydantic_objects=output_type.__args__) # type: ignore # TODO: fix this + if output_type is str: + return StrOutputParser() + if output_type is bool: + return BoolOutputParser() + if issubclass(output_type, ParserBaseModel): + return output_type.output_parser() # type: ignore + + from pydantic import BaseModel + + if issubclass(output_type, BaseModel): + return RetryJsonPydanticParser(pydantic_object=output_type, retry=retry, retry_llm=llm) + else: + raise SyntaxError(f"Output Type is not supported: {output_type}") From 5d88f8d859c626ca239f8034687f41c6aed5d6c1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 156/451] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20up=20imports=20s?= =?UTF-8?q?ignature.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py index 7377ed0..1d0e5ae 100644 --- a/src/funcchain/schema/signature.py +++ b/src/funcchain/schema/signature.py @@ -1,8 +1,6 @@ from typing import Any, Generic, TypeVar from langchain.pydantic_v1 import BaseModel, Field - -# from langchain_core.language_models import BaseChatModel from langchain_core.messages import BaseMessage from ..backend.settings import FuncchainSettings, settings From 2f43af26a1c591fb871c098393cfa54ddde34e30 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 157/451] =?UTF-8?q?=E2=9C=A8=20Add=20output=20types=20expo?= =?UTF-8?q?rts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/funcchain/syntax/__init__.py b/src/funcchain/syntax/__init__.py index 3bafb1a..c10254d 100644 --- a/src/funcchain/syntax/__init__.py +++ b/src/funcchain/syntax/__init__.py @@ -2,9 +2,12 @@ """ from .decorators import runnable from .executable import achain, chain +from .output_types import CodeBlock, Error __all__ = [ "chain", "achain", "runnable", + "CodeBlock", + "Error", ] From f4b7a7a5465b09f2589e7cd985c11533456ceb6c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 158/451] =?UTF-8?q?=F0=9F=93=A6=20Add=20RouterChat=20to=20?= =?UTF-8?q?=5F=5Finit=5F=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/funcchain/syntax/components/__init__.py b/src/funcchain/syntax/components/__init__.py index e69de29..848903b 100644 --- a/src/funcchain/syntax/components/__init__.py +++ b/src/funcchain/syntax/components/__init__.py @@ -0,0 +1,5 @@ +from .router import RouterChat + +__all__ = [ + "RouterChat", +] From 73674f90f3c271bc0f627e6aef5ceef2f45f600e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 159/451] =?UTF-8?q?=F0=9F=9A=9A=20Relocate=20gather=5Fsign?= =?UTF-8?q?ature=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/syntax/decorators.py b/src/funcchain/syntax/decorators.py index de3441c..ea92487 100644 --- a/src/funcchain/syntax/decorators.py +++ b/src/funcchain/syntax/decorators.py @@ -5,9 +5,9 @@ from langchain_core.runnables import Runnable from ..backend.compiler import compile_chain +from ..backend.meta_inspect import gather_signature from ..backend.settings import SettingsOverride, create_local_settings from ..schema.signature import Signature -from .meta_inspect import gather_signature OutputT = TypeVar("OutputT") From ed7f9166527c4733deb6d9a6d319dfa4dba9f931 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 160/451] =?UTF-8?q?=F0=9F=94=84=20Rearrange=20import=20sta?= =?UTF-8?q?tements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py index 44ab616..0e648df 100644 --- a/src/funcchain/syntax/executable.py +++ b/src/funcchain/syntax/executable.py @@ -8,14 +8,14 @@ from langchain_core.runnables import Runnable from ..backend.compiler import compile_chain -from ..backend.settings import SettingsOverride, create_local_settings -from ..schema.signature import Signature -from .meta_inspect import ( +from ..backend.meta_inspect import ( from_docstring, get_output_type, get_parent_frame, kwargs_from_parent, ) +from ..backend.settings import SettingsOverride, create_local_settings +from ..schema.signature import Signature ChainOut = TypeVar("ChainOut") From 1abeb617921e013ae84b6f2018e4913647d8ce20 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 161/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20input=20type=20def?= =?UTF-8?q?initions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/input_types.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/funcchain/syntax/input_types.py diff --git a/src/funcchain/syntax/input_types.py b/src/funcchain/syntax/input_types.py new file mode 100644 index 0000000..e98f4a0 --- /dev/null +++ b/src/funcchain/syntax/input_types.py @@ -0,0 +1,15 @@ +from typing import TypedDict + +from langchain_core.chat_history import BaseChatMessageHistory + + +class ImageURL(TypedDict): + """Funcchain type for passing an image as external url.""" + + url: str + + +class ChatHistory(BaseChatMessageHistory): + """Funcchain Type Wrapper for detecting ChatHistorys.""" + + ... From 45b9b09ccfa0e16c263307a88a71acb3ec0c943a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 162/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20output=5Ftypes.?= =?UTF-8?q?py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/output_types.py | 88 ++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/funcchain/syntax/output_types.py diff --git a/src/funcchain/syntax/output_types.py b/src/funcchain/syntax/output_types.py new file mode 100644 index 0000000..829a3e0 --- /dev/null +++ b/src/funcchain/syntax/output_types.py @@ -0,0 +1,88 @@ +import json +import re +from typing import Optional + +from langchain_core.exceptions import OutputParserException +from langchain_core.output_parsers import BaseLLMOutputParser +from pydantic import BaseModel, Field +from typing_extensions import Self + + +class ParserBaseModel(BaseModel): + @classmethod + def output_parser(cls) -> BaseLLMOutputParser[Self]: + from ..parser.custom import CustomPydanticOutputParser + + return CustomPydanticOutputParser(pydantic_object=cls) + + @classmethod + def parse(cls, text: str) -> Self: + """Override for custom parsing.""" + match = re.search(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) + json_str = "" + if match: + json_str = match.group() + json_object = json.loads(json_str, strict=False) + return cls.model_validate(json_object) + + @staticmethod + def format_instructions() -> str: + return ( + "Please respond with a json result matching the following schema:" + "\n\n```schema\n{schema}\n```\n" + "Do not repeat the schema. Only respond with the result." + ) + + @staticmethod + def custom_grammar() -> str | None: + return None + + +class CodeBlock(ParserBaseModel): + code: str + language: Optional[str] = None + + @classmethod + def parse(cls, text: str) -> "CodeBlock": + matches = re.finditer(r"```(?P\w+)?\n?(?P.*?)```", text, re.DOTALL) + for match in matches: + groupdict = match.groupdict() + groupdict["language"] = groupdict.get("language", None) + + # custom markdown fix + if groupdict["language"] == "markdown": + t = text.split("```markdown")[1] + return cls( + language="markdown", + code=t[: -(len(t.split("```")[-1]) + 3)], + ) + + return cls(**groupdict) + + return cls(code=text) # TODO: fix this hack + raise OutputParserException("Invalid codeblock") + + @staticmethod + def format_instructions() -> str: + return "Answer with a codeblock." + + @staticmethod + def custom_grammar() -> str | None: + return 'root ::= "```" ([^`] | "`" [^`] | "``" [^`])* "```"' + + def __str__(self) -> str: + return self.code + + +class Error(BaseModel): + """ + Fallback function for invalid input. + If you are unsure on what function to call, use this error function as fallback. + This will tell the user that the input is not valid. + """ + + title: str = Field(description="CamelCase Name titeling the error") + description: str = Field(..., description="Short description of the unexpected situation") + + def __raise__(self) -> None: + raise Exception(self.description) From 99521adf5bdfb77f00a94f89ac4863ebf59ffab7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 163/451] =?UTF-8?q?=E2=9C=A8=20Add=20exception=20raising?= =?UTF-8?q?=20utility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/funcchain/utils/__init__.py b/src/funcchain/utils/__init__.py index e69de29..6505144 100644 --- a/src/funcchain/utils/__init__.py +++ b/src/funcchain/utils/__init__.py @@ -0,0 +1,5 @@ +from typing import NoReturn + + +def raiser(e: Exception | str) -> NoReturn: + raise e if isinstance(e, Exception) else Exception(e) From bbfc4f344cab29509d6635c39b11c1f499924d29 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 164/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20funcs.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/funcs.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/funcchain/utils/funcs.py diff --git a/src/funcchain/utils/funcs.py b/src/funcchain/utils/funcs.py deleted file mode 100644 index 7dcc68c..0000000 --- a/src/funcchain/utils/funcs.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import NoReturn - -from tiktoken import encoding_for_model - - -def raiser(e: Exception | str) -> NoReturn: - raise e if isinstance(e, Exception) else Exception(e) - - -def count_tokens(text: str, model: str = "gpt-4") -> int: - return len(encoding_for_model(model).encode(text)) From f306973ddb7a4c1b8e46ee564229310f26b4c5cd Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 15 Jan 2024 21:05:34 +0100 Subject: [PATCH 165/451] =?UTF-8?q?=E2=9C=A8=20Add=20token=20counter=20uti?= =?UTF-8?q?lity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/token_counter.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/funcchain/utils/token_counter.py diff --git a/src/funcchain/utils/token_counter.py b/src/funcchain/utils/token_counter.py new file mode 100644 index 0000000..6c45f28 --- /dev/null +++ b/src/funcchain/utils/token_counter.py @@ -0,0 +1,7 @@ +def count_tokens(text: str, model: str = "gpt-4") -> int: + if "gpt-4" in model: + from tiktoken import encoding_for_model + + return len(encoding_for_model(model).encode(text)) + else: + raise NotImplementedError("Please sumbmit a PR or write an issue with your desired model.") From 2bce305f6d800bb5ba6999c3f3c02321f33c6fb0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 166/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MODELS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MODELS.md b/MODELS.md index 9cdccf4..0e1a951 100644 --- a/MODELS.md +++ b/MODELS.md @@ -5,7 +5,7 @@ You can set the `settings.llm` with any ChatModel the LangChain library. ```python -from langchain.chat_models import AzureChatOpenAI +from langchain_openai.chat_models import AzureChatOpenAI settings.llm = AzureChatOpenAI(...) ``` From 735331481df1a6f0df2502f862dd3b7a835fc7ad Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 167/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20chatgpt=20mem?= =?UTF-8?q?ory=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/chatgpt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/chatgpt.py b/examples/chatgpt.py index 0bc3340..c1a34ea 100644 --- a/examples/chatgpt.py +++ b/examples/chatgpt.py @@ -2,8 +2,7 @@ Simple chatgpt rebuild with memory/history. """ from funcchain import chain, settings -from funcchain.backend.streaming import stream_to -from langchain.memory import ChatMessageHistory +from funcchain.utils.memory import ChatMessageHistory settings.llm = "openai/gpt-4" @@ -31,8 +30,7 @@ def chat_loop() -> None: print("\033c") continue - with stream_to(print): - ask(query) + ask(query) if __name__ == "__main__": From ea70cae22ce2a1282b1f03a4ad5fc4362d5c0094 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 168/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/decorator.py b/examples/decorator.py index ce217f6..7f29c7f 100644 --- a/examples/decorator.py +++ b/examples/decorator.py @@ -1,7 +1,7 @@ from funcchain.syntax import chain, runnable -from langchain.embeddings.openai import OpenAIEmbeddings from langchain_community.vectorstores.faiss import FAISS from langchain_core.runnables import Runnable, RunnablePassthrough +from langchain_openai.embeddings import OpenAIEmbeddings @runnable From 6c882d38fbb9f83cfe900c041fae32872f6f722a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 169/451] =?UTF-8?q?=F0=9F=94=84=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/experiments/dynamic_model_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/experiments/dynamic_model_generation.py b/examples/experiments/dynamic_model_generation.py index 21b3de3..1da1a5b 100644 --- a/examples/experiments/dynamic_model_generation.py +++ b/examples/experiments/dynamic_model_generation.py @@ -1,6 +1,6 @@ from funcchain import chain, settings from funcchain.syntax.output_types import CodeBlock -from langchain.document_loaders import WebBaseLoader +from langchain_community.document_loaders import WebBaseLoader from pydantic import BaseModel from rich import print From 3c04b391e4e7f673b13a58e1e57c8ddef89e6f73 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 170/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20streaming=20set?= =?UTF-8?q?tings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pydantic_validation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/pydantic_validation.py b/examples/pydantic_validation.py index 7dea97b..133d1bb 100644 --- a/examples/pydantic_validation.py +++ b/examples/pydantic_validation.py @@ -1,8 +1,8 @@ from funcchain import chain, settings -from funcchain.backend.streaming import stream_to from pydantic import BaseModel, field_validator settings.llm = "ollama/openchat" +settings.console_stream = True class Task(BaseModel): @@ -32,6 +32,5 @@ def gather_infos(user_description: str) -> Task: if __name__ == "__main__": - with stream_to(print): - task = gather_infos("cleanup the kitchen") + task = gather_infos("cleanup the kitchen") print(f"{task=}") From d2f1900a7338b1080d1e1520fd985b9f335547ef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 171/451] =?UTF-8?q?=E2=9C=A8=20Add=20stream=5Frunnables=20?= =?UTF-8?q?example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/stream_runnables.py | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 examples/stream_runnables.py diff --git a/examples/stream_runnables.py b/examples/stream_runnables.py new file mode 100644 index 0000000..1b592e5 --- /dev/null +++ b/examples/stream_runnables.py @@ -0,0 +1,95 @@ +from typing import AsyncIterator, Iterator + +from funcchain import chain, settings +from funcchain.syntax import runnable +from funcchain.syntax.components import RouterChat +from funcchain.syntax.components.handler import BasicChatHandler +from funcchain.utils.msg_tools import msg_to_str +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.runnables import RunnableGenerator, RunnableSerializable + +# settings.llm = "ollama/openchat" + + +@runnable +def animal_poem(animal: str) -> str: + """ + Write a long poem about the animal. + """ + return chain() + + +def split_into_list( + input: Iterator[str], +) -> Iterator[list[str]]: + buffer = "" + for chunk in input: + buffer += chunk + while "\n" in buffer: + comma_index = buffer.index("\n") + yield [buffer[:comma_index].strip()] + buffer = buffer[comma_index + 1 :] + yield [buffer.strip()] + + +async def asplit_into_list( + input: AsyncIterator[str], +) -> AsyncIterator[list[str]]: + buffer = "" + async for chunk in input: + buffer += chunk + while "\n" in buffer: + comma_index = buffer.index("\n") + yield [buffer[:comma_index].strip()] + buffer = buffer[comma_index + 1 :] + yield [buffer.strip()] + + +animal_list_chain = animal_poem | RunnableGenerator(transform=split_into_list, atransform=asplit_into_list) + + +def convert_to_ai_message(input: Iterator[list[str]]) -> Iterator[AIMessage]: + for chunk in input: + yield AIMessage(content=chunk[0]) + + +async def aconvert_to_ai_message(input: AsyncIterator[list[str]]) -> AsyncIterator[AIMessage]: + async for chunk in input: + yield AIMessage(content=chunk[0]) + + +animal_chat: RunnableSerializable[HumanMessage, AIMessage] = ( + { + "animal": lambda x: msg_to_str(x), # type: ignore + } + | animal_list_chain + | RunnableGenerator(transform=convert_to_ai_message, atransform=aconvert_to_ai_message) # type: ignore +) + + +chat = RouterChat( + { + "animal": { + "handler": animal_chat, + "description": "If the user gives an animal, call this handler.", + }, + "default": { + "handler": BasicChatHandler( + system_message="You are a powerful AI assistant. " + "Always mention that the user should start funcchain on github." + ), + "description": "Any other request.", + }, + } +) + + +def main() -> None: + for chunk in chat.stream(HumanMessage(content="Hey whatsup?")): + if isinstance(chunk, AIMessage): + print(chunk.content, flush=True) + if isinstance(chunk, str): + print(chunk, flush=True, end="") + + +main() From 5a039a74b552af27796c8d351567f1f8d0223ef6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 172/451] =?UTF-8?q?=F0=9F=94=84=20Update=20settings=20and?= =?UTF-8?q?=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/vision.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/vision.py b/examples/vision.py index b91967a..a73b580 100644 --- a/examples/vision.py +++ b/examples/vision.py @@ -2,7 +2,9 @@ from PIL import Image from pydantic import BaseModel, Field -settings.llm = "openai/gpt-4-vision-preview" +# settings.llm = "openai/gpt-4-vision-preview" +settings.llm = "ollama/bakllava" +settings.console_stream = True class AnalysisResult(BaseModel): @@ -23,10 +25,8 @@ def analyse_image(image: Image.Image) -> AnalysisResult: if __name__ == "__main__": example_image = Image.open("examples/assets/old_chinese_temple.jpg") - from funcchain.backend.streaming import stream_to - with stream_to(print): - result = analyse_image(example_image) + result = analyse_image(example_image) print("Theme:", result.theme) print("Description:", result.description) From c80375c6a4108572e8380c32f4541d8a7a79b755 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 173/451] =?UTF-8?q?=F0=9F=94=A7=20Bump=20funcchain=20versi?= =?UTF-8?q?on,=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e1e2810..1f101c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,18 @@ [project] name = "funcchain" -version = "0.2.0-alpha.2" +version = "0.2.0-alpha.3" description = "🔖 write prompts as python functions" authors = [ { name = "Shroominic", email = "contact@shroominic.com" } ] dependencies = [ "langchain_core>=0.1.10", - "langchain_openai>=0.0.2.post1", + "langchain_openai>=0.0.2.post1", # TODO: make optional "pydantic-settings>=2", "docstring-parser>=0.15", "rich>=13", "jinja2>=3", - "pillow>=10", + "pillow>=10", # TODO: make optional ] license = "MIT" readme = "README.md" @@ -54,10 +54,15 @@ dev-dependencies = [ [project.optional-dependencies] openai = [ - "langchain[openai]", + "langchain_openai", +] +ollama = [ + "langchain_community", ] all = [ "funcchain[openai]", + "funcchain[ollama]", + "langchain", ] [tool.hatch.metadata] From 075f88d724904d64992d31a9bce971332ac46fd5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 174/451] =?UTF-8?q?=E2=9C=A8=20Add=20types-pyyaml=20depend?= =?UTF-8?q?ency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.lock b/requirements-dev.lock index 589c9de..db438bb 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -98,6 +98,7 @@ tenacity==8.2.3 tiktoken==0.5.2 tqdm==4.66.1 traitlets==5.14.0 +types-pyyaml==6.0.12.12 typing-extensions==4.8.0 typing-inspect==0.9.0 urllib3==2.1.0 From d92712688e01d535ff121ad5a2cb41a39efc7831 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 175/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20autotu?= =?UTF-8?q?ne=20init=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/autotune/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/funcchain/autotune/__init__.py diff --git a/src/funcchain/autotune/__init__.py b/src/funcchain/autotune/__init__.py deleted file mode 100644 index e69de29..0000000 From 0ce8611b1dbb4767d7672085eb9ed19081786a0c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 176/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20compiler=20fu?= =?UTF-8?q?nctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index cdfcb2e..b1739c5 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -70,17 +70,17 @@ def create_union_chain( return prompt | llm | OpenAIFunctionPydanticUnionParser(output_types=output_types) -def parse_function_to_pydantic( +def patch_openai_function_to_pydantic( llm: BaseChatModel, output_type: type[BaseModel], input_kwargs: dict[str, str], -) -> BaseGenerationOutputParser: +) -> tuple[BaseChatModel, BaseGenerationOutputParser]: input_kwargs["format_instructions"] = f"Extract to {output_type.__name__}." functions = pydantic_to_functions(output_type) llm = llm.bind(**functions) # type: ignore - return OpenAIFunctionPydanticParser(pydantic_schema=output_type) + return llm, OpenAIFunctionPydanticParser(pydantic_schema=output_type) def create_chain( @@ -90,7 +90,7 @@ def create_chain( context: list[BaseMessage], memory: BaseChatMessageHistory, settings: FuncchainSettings, - input_kwargs: dict[str, str], + input_args: list[tuple[str, type]], ) -> Runnable[dict[str, str], ChainOutput]: """ Compile a langchain runnable chain from the funcchain syntax. @@ -101,6 +101,21 @@ def create_chain( parser = parser_for(output_type, retry=settings.retry_parse, llm=llm) + # handle input arguments + prompt_args: list[str] = [] + pydantic_args: list[str] = [] + special_args: list[tuple[str, type]] = [] + + for i in input_args: + if i[1] is str: + prompt_args.append(i[0]) + if i[1] is BaseModel: + pydantic_args.append(i[0]) + else: + special_args.append(i) + + input_kwargs = {k: "" for k in prompt_args + pydantic_args} + # add format instructions for parser f_instructions = None if parser and (settings.streaming or not is_openai_function_model(llm)): @@ -156,7 +171,7 @@ def create_chain( if settings.streaming and hasattr(llm, "model_kwargs"): llm.model_kwargs = {"response_format": {"type": "json_object"}} else: - parser = parse_function_to_pydantic(llm, output_type, input_kwargs) + llm, parser = patch_openai_function_to_pydantic(llm, output_type, input_kwargs) assert parser is not None return chat_prompt | llm | parser @@ -172,9 +187,8 @@ def compile_chain(signature: Signature[ChainOutput]) -> Runnable[dict[str, str], SystemMessage(content=""), ] ).pop() - input_kwargs = {k: "" for k in signature.input_args} - from langchain.memory import ChatMessageHistory + from ..utils.memory import ChatMessageHistory memory = ChatMessageHistory(messages=signature.history) @@ -185,7 +199,7 @@ def compile_chain(signature: Signature[ChainOutput]) -> Runnable[dict[str, str], signature.history, memory, signature.settings, - input_kwargs, + signature.input_args, ) From 981e7d329d86fa9f92699a572d7b067d65b3f015 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 177/451] =?UTF-8?q?=E2=9C=A8=20Enhance=20signature=20intro?= =?UTF-8?q?spection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/meta_inspect.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/funcchain/backend/meta_inspect.py b/src/funcchain/backend/meta_inspect.py index cd38b86..5b909ad 100644 --- a/src/funcchain/backend/meta_inspect.py +++ b/src/funcchain/backend/meta_inspect.py @@ -55,12 +55,21 @@ def kwargs_from_parent() -> dict[str, str]: return get_parent_frame(FUNC_DEPTH - 1).frame.f_locals -def gather_signature(f: types.FunctionType) -> dict[str, Any]: +def args_from_parent() -> list[tuple[str, type]]: + """ + Get input args with type hints from parent function + """ + # TODO: implement + + +def gather_signature( + f: types.FunctionType, +) -> dict[str, str | list[tuple[str, type]] | type]: """ Gather the signature of the parent caller function. """ return { "instruction": from_docstring(f), - "input_args": list(f.__code__.co_varnames[: f.__code__.co_argcount]), + "input_args": [(arg, f.__annotations__[arg]) for arg in f.__code__.co_varnames[: f.__code__.co_argcount]], "output_type": get_output_type(f), } From 86c9a7cc12271fc7c34774ed7535faa122f2dbd3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 178/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/abilities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/funcchain/model/abilities.py b/src/funcchain/model/abilities.py index a808879..82b94b6 100644 --- a/src/funcchain/model/abilities.py +++ b/src/funcchain/model/abilities.py @@ -1,6 +1,8 @@ from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage +from ..model.llm_overrides import ChatOllama + verified_openai_function_models = [ "gpt-4", "gpt-4-0613", @@ -25,7 +27,7 @@ def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: - from langchain_community.chat_models import ChatOllama, ChatOpenAI # TODO: fix make this optional + from langchain_openai.chat_models import ChatOpenAI if not isinstance(llm, BaseChatModel): return "base_model" From 087fc153538238901a43bfe3b8f4680ce0779809 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 179/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20ChatOllama=20?= =?UTF-8?q?import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/llm_overrides.py | 84 +++++++++++++++------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/src/funcchain/model/llm_overrides.py b/src/funcchain/model/llm_overrides.py index 6df2fe6..eebebed 100644 --- a/src/funcchain/model/llm_overrides.py +++ b/src/funcchain/model/llm_overrides.py @@ -1,43 +1,51 @@ from typing import Any, Dict, Optional -from langchain_community.chat_models import ChatOllama as _ChatOllama from langchain_core.pydantic_v1 import validator +try: + from langchain_community.chat_models import ChatOllama as _ChatOllama -class ChatOllama(_ChatOllama): - grammar: Optional[str] = None - """ - The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the model output. - """ - - @validator("grammar") - def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: - if v is not None and "root ::=" not in v: - raise ValueError("Grammar must contain a root rule.") - return v - - @property - def _default_params(self) -> Dict[str, Any]: - """Get the default parameters for calling Ollama.""" - return { - "model": self.model, - "format": self.format, - "options": { - "mirostat": self.mirostat, - "mirostat_eta": self.mirostat_eta, - "mirostat_tau": self.mirostat_tau, - "num_ctx": self.num_ctx, - "num_gpu": self.num_gpu, - "num_thread": self.num_thread, - "repeat_last_n": self.repeat_last_n, - "repeat_penalty": self.repeat_penalty, - "temperature": self.temperature, - "stop": self.stop, - "tfs_z": self.tfs_z, - "top_k": self.top_k, - "top_p": self.top_p, - "grammar": self.grammar, # added - }, - "system": self.system, - "template": self.template, - } + class ChatOllama(_ChatOllama): # type: ignore + grammar: Optional[str] = None + """ + The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the output. + """ + + @validator("grammar") + def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: + if v is not None and "root ::=" not in v: + raise ValueError("Grammar must contain a root rule.") + return v + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling Ollama.""" + return { + "model": self.model, + "format": self.format, + "options": { + "mirostat": self.mirostat, + "mirostat_eta": self.mirostat_eta, + "mirostat_tau": self.mirostat_tau, + "num_ctx": self.num_ctx, + "num_gpu": self.num_gpu, + "num_thread": self.num_thread, + "repeat_last_n": self.repeat_last_n, + "repeat_penalty": self.repeat_penalty, + "temperature": self.temperature, + "stop": self.stop, + "tfs_z": self.tfs_z, + "top_k": self.top_k, + "top_p": self.top_p, + "grammar": self.grammar, # added + }, + "system": self.system, + "template": self.template, + } + + +except ImportError: + + class ChatOllama: # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + raise ImportError("Please install langchain_community to use ChatOllama.") From 2b121b15fc92110597a9ccf8083cbb0d84504784 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 180/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20grammars.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/grammars.py | 130 ------------------------------- 1 file changed, 130 deletions(-) delete mode 100644 src/funcchain/parser/grammars.py diff --git a/src/funcchain/parser/grammars.py b/src/funcchain/parser/grammars.py deleted file mode 100644 index 47d785d..0000000 --- a/src/funcchain/parser/grammars.py +++ /dev/null @@ -1,130 +0,0 @@ -import json -import re -from typing import Type - -from pydantic import BaseModel - -SPACE_RULE = '" "?' - -PRIMITIVE_RULES = { - "boolean": '("true" | "false") space', - "number": '("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space', - "integer": '("-"? ([0-9] | [1-9] [0-9]*)) space', - "string": r""" "\"" ( - [^"\\] | - "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) - )* "\"" space """, - "null": '"null" space', -} - -INVALID_RULE_CHARS_RE = re.compile(r"[^a-zA-Z0-9-]+") -GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"]') -GRAMMAR_LITERAL_ESCAPES = {"\r": "\\r", "\n": "\\n", '"': '\\"'} - - -class SchemaConverter: - def __init__(self, prop_order: dict, defs: dict) -> None: - self._prop_order = prop_order - self._defs = defs - self._rules = {"space": SPACE_RULE} - - def _format_literal(self, literal: str) -> str: - escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub( - lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)), # type: ignore - json.dumps(literal), - ) - return f'"{escaped}"' - - def _add_rule(self, name: str, rule: str) -> str: - esc_name = INVALID_RULE_CHARS_RE.sub("-", name) - if esc_name not in self._rules or self._rules[esc_name] == rule: - key = esc_name - else: - i = 0 - while f"{esc_name}{i}" in self._rules: - i += 1 - key = f"{esc_name}{i}" - self._rules[key] = rule - return key - - def visit(self, schema: dict, name: str) -> str: - schema_type = schema.get("type") - rule_name = name or "root" - - if "$ref" in schema: - ref_name = schema["$ref"].split("/")[-1] - assert ref_name in self._defs, f"Unresolved reference: {schema['$ref']}" - return self.visit(self._defs[ref_name], ref_name) - - elif "oneOf" in schema or "anyOf" in schema: - rule = " | ".join( - ( - self.visit(alt_schema, f'{name}{"-" if name else ""}{i}') - for i, alt_schema in enumerate(schema.get("oneOf") or schema["anyOf"]) - ) - ) - return self._add_rule(rule_name, rule) - - elif "allOf" in schema: - rule = " ".join( - ( - self.visit(sub_schema, f'{name}{"-" if name else ""}{i}') - for i, sub_schema in enumerate(schema["allOf"]) - ) - ) - return self._add_rule(rule_name, rule) - - elif "const" in schema: - return self._add_rule(rule_name, self._format_literal(schema["const"])) - - elif "enum" in schema: - rule = " | ".join((self._format_literal(v) for v in schema["enum"])) - return self._add_rule(rule_name, rule) - - elif schema_type == "object" and "properties" in schema: - # TODO: `required` keyword - prop_order = self._prop_order - prop_pairs = sorted( - schema["properties"].items(), - # sort by position in prop_order (if specified) then by key - key=lambda kv: (prop_order.get(kv[0], len(prop_order)), kv[0]), - ) - - rule = '"{" space' - for i, (prop_name, prop_schema) in enumerate(prop_pairs): - prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}') - if i > 0: - rule += ' "," space' - rule += rf' {self._format_literal(prop_name)} space ":" space {prop_rule_name}' - rule += ' "}" space' - - return self._add_rule(rule_name, rule) - - elif schema_type == "array" and "items" in schema: - # TODO `prefixItems` keyword - item_rule_name = self.visit(schema["items"], f'{name}{"-" if name else ""}item') - rule = f'"[" space ({item_rule_name} ("," space {item_rule_name})*)? "]" space' - return self._add_rule(rule_name, rule) - - else: - assert schema_type in PRIMITIVE_RULES, f"Unrecognized schema: {schema}" - return self._add_rule( - "root" if rule_name == "root" else schema_type, - PRIMITIVE_RULES[schema_type], - ) - - def format_grammar(self) -> str: - return "\n".join((f"{name} ::= {rule}" for name, rule in self._rules.items())) - - -def schema_to_grammar(json_schema: dict) -> str: - schema = json_schema - prop_order = {name: idx for idx, name in enumerate(schema["properties"].keys())} - defs = schema.get("$defs", {}) - converter = SchemaConverter(prop_order, defs) - converter.visit(schema, "") - return converter.format_grammar() - - -def pydantic_to_grammar(model: Type[BaseModel]) -> str: - return schema_to_grammar(model.model_json_schema()) From 8d260e8cd82f376ff6e5e9784072785c2d3e71ea Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 181/451] =?UTF-8?q?=F0=9F=94=84=20Update=20imports=20and?= =?UTF-8?q?=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py index 1d0e5ae..62bc832 100644 --- a/src/funcchain/schema/signature.py +++ b/src/funcchain/schema/signature.py @@ -1,7 +1,7 @@ from typing import Any, Generic, TypeVar -from langchain.pydantic_v1 import BaseModel, Field from langchain_core.messages import BaseMessage +from langchain_core.pydantic_v1 import BaseModel, Field from ..backend.settings import FuncchainSettings, settings @@ -16,7 +16,7 @@ class Signature(BaseModel, Generic[T]): instruction: str """ Prompt instruction to the language model. """ - input_args: list[str] = Field(default_factory=list) + input_args: list[tuple[str, type]] = Field(default_factory=list) """ List of input arguments for the prompt template. """ # TODO collect types from input_args @@ -26,7 +26,6 @@ class Signature(BaseModel, Generic[T]): # -> e.g. SettingsOverride overrides the default settings # -> e.g. Callbacks adds custom callbacks # -> e.g. SystemMessage adds a system message - # maybe do input_args: list[tuple[str, type]] = Field(default_factory=list) output_type: type[T] """ Type to parse the output into. """ From 84a3df438f98b8922f7c1777329d686900c630b0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 182/451] =?UTF-8?q?=E2=9C=A8=20Add=20BasicChatHandler=20co?= =?UTF-8?q?mponent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/handler.py | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/funcchain/syntax/components/handler.py diff --git a/src/funcchain/syntax/components/handler.py b/src/funcchain/syntax/components/handler.py new file mode 100644 index 0000000..ae75231 --- /dev/null +++ b/src/funcchain/syntax/components/handler.py @@ -0,0 +1,61 @@ +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.runnables import Runnable +from langchain_core.runnables.history import RunnableWithMessageHistory + +from ...backend.settings import settings +from ...model.defaults import univeral_model_selector +from ...utils.msg_tools import msg_to_str + +UniversalLLM = BaseChatModel | str | None + + +def load_universal_llm(llm: UniversalLLM) -> BaseChatModel: + if isinstance(llm, str): + settings.llm = llm + llm = None + if not llm: + llm = univeral_model_selector(settings) + return llm + + +# def history_handler(input: Iterator[Any]) -> Iterator[Any]: + +# for chunk in input: +# yield chunk + + +def BasicChatHandler( + *, + llm: UniversalLLM = None, + chat_history: BaseChatMessageHistory | None = None, + system_message: str = "", +) -> Runnable[HumanMessage, AIMessage]: + if chat_history is None: + from ...utils.memory import ChatMessageHistory + + chat_history = ChatMessageHistory() + + llm = load_universal_llm(llm) + + handler_chain = ( + ChatPromptTemplate.from_messages( + [ + *(("system", system_message) if system_message else []), + MessagesPlaceholder(variable_name="history"), + ("human", "{user_msg}"), + ] + ) + | llm + ) + return { + # todo handle images + "user_msg": lambda x: msg_to_str(x), + } | RunnableWithMessageHistory( + handler_chain, # type: ignore + get_session_history=lambda _: chat_history, + input_messages_key="user_msg", + history_messages_key="history", + ) From 762cbae33f52f457de9627eb4ab1d4ed91150e57 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 183/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20router=20runn?= =?UTF-8?q?able=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/router.py | 36 +++++++++-------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/funcchain/syntax/components/router.py b/src/funcchain/syntax/components/router.py index 57c8377..d58b291 100644 --- a/src/funcchain/syntax/components/router.py +++ b/src/funcchain/syntax/components/router.py @@ -7,10 +7,11 @@ from langchain_core.runnables import ( RouterRunnable, Runnable, + RunnableConfig, RunnableLambda, + RunnablePassthrough, RunnableSerializable, ) -from langchain_core.runnables.config import RunnableConfig from typing_extensions import TypedDict from ...utils.msg_tools import msg_to_str @@ -46,29 +47,20 @@ def __init__( @property def runnable(self) -> RunnableSerializable[HumanMessage, AIMessage]: + # TODO: update history somewhere return { - "input": lambda x: x, - "output": { - "input": lambda x: x, - "key": { - # todo "images": x.images, - "user_request": msg_to_str, - "routes": lambda _: self._routes_repr(), - } - # route selection - | self._selector() - | RunnableLambda(lambda x: x.selector.value), + "input": RunnablePassthrough(), + "key": { + # todo "images": x.images, + "user_request": msg_to_str, + "routes": lambda _: self._routes_repr(), } - # route invocation - | RouterRunnable( - runnables={name: run["handler"] for name, run in self.routes.items()}, - ), - # update history - } | RunnableLambda( - lambda x: (self.history.add_message(x["input"]) or self.history.add_message(x["output"]) or x["output"]) - if self.history - else x["output"] - ) + # route selection + | self._selector() + | (lambda x: x.selector.value), + } | RouterRunnable( + runnables={name: run["handler"] for name, run in self.routes.items()}, + ) # maybe add auto conversion of strings to AI Messages/Chunks def _selector(self) -> Runnable[dict[str, str], Any]: RouteChoices = Enum( # type: ignore From d41834e89c554e6daa50542b96fe4283f4b34e35 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 184/451] =?UTF-8?q?=E2=9C=A8=20Refactor=20type=20handling,?= =?UTF-8?q?=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py index 0e648df..06ce656 100644 --- a/src/funcchain/syntax/executable.py +++ b/src/funcchain/syntax/executable.py @@ -1,6 +1,5 @@ -from typing import TypeVar +from typing import Any, TypeVar -from langchain.memory import ChatMessageHistory from langchain_core.callbacks.base import Callbacks from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.language_models import BaseChatModel @@ -9,6 +8,7 @@ from ..backend.compiler import compile_chain from ..backend.meta_inspect import ( + args_from_parent, from_docstring, get_output_type, get_parent_frame, @@ -16,8 +16,7 @@ ) from ..backend.settings import SettingsOverride, create_local_settings from ..schema.signature import Signature - -ChainOut = TypeVar("ChainOut") +from ..utils.memory import ChatMessageHistory def chain( @@ -27,27 +26,30 @@ def chain( memory: BaseChatMessageHistory | None = None, settings_override: SettingsOverride = {}, **input_kwargs: str, -) -> ChainOut: # type: ignore +) -> Any: """ Generate response of llm for provided instructions. """ settings = create_local_settings(settings_override) callbacks: Callbacks = None output_type = get_output_type() + input_args: list[tuple[str, type]] = args_from_parent() memory = memory or ChatMessageHistory() input_kwargs.update(kwargs_from_parent()) + + # todo maybe this should be done in the prompt processor? system = system or settings.system_prompt instruction = instruction or from_docstring() sig: Signature = Signature( instruction=instruction, - input_args=list(input_kwargs.keys()), + input_args=input_args, output_type=output_type, history=context, settings=settings, ) - chain: Runnable[dict[str, str], ChainOut] = compile_chain(sig) + chain: Runnable[dict[str, str], Any] = compile_chain(sig) result = chain.invoke(input_kwargs, {"run_name": get_parent_frame(3).function, "callbacks": callbacks}) if memory and isinstance(result, str): @@ -64,13 +66,14 @@ async def achain( memory: BaseChatMessageHistory | None = None, settings_override: SettingsOverride = {}, **input_kwargs: str, -) -> ChainOut: +) -> Any: """ Asyncronously generate response of llm for provided instructions. """ settings = create_local_settings(settings_override) callbacks: Callbacks = None output_type = get_output_type() + input_args: list[tuple[str, type]] = args_from_parent() memory = memory or ChatMessageHistory() input_kwargs.update(kwargs_from_parent()) @@ -81,12 +84,12 @@ async def achain( sig: Signature = Signature( instruction=instruction, - input_args=list(input_kwargs.keys()), + input_args=input_args, output_type=output_type, history=context, settings=settings, ) - chain: Runnable[dict[str, str], ChainOut] = compile_chain(sig) + chain: Runnable[dict[str, str], Any] = compile_chain(sig) result = await chain.ainvoke(input_kwargs, {"run_name": get_parent_frame(5).function, "callbacks": callbacks}) if memory and isinstance(result, str): @@ -96,6 +99,9 @@ async def achain( return result +ChainOut = TypeVar("ChainOut") + + def compile_runnable( instruction: str, output_type: type[ChainOut], @@ -113,10 +119,11 @@ def compile_runnable( instruction = "\n" + instruction settings = create_local_settings(settings_override) context = [SystemMessage(content=system)] + context + _input_args: list[tuple[str, type]] = [(arg, str) for arg in input_args] sig: Signature = Signature( instruction=instruction, - input_args=input_args, + input_args=_input_args, output_type=output_type, history=context, settings=settings, From d39ab032ab39e6c26eea4088b62e566c41471514 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:54 +0000 Subject: [PATCH 185/451] =?UTF-8?q?=E2=9C=A8=20Add=20TODO=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/input_types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/funcchain/syntax/input_types.py b/src/funcchain/syntax/input_types.py index e98f4a0..be7374d 100644 --- a/src/funcchain/syntax/input_types.py +++ b/src/funcchain/syntax/input_types.py @@ -3,12 +3,14 @@ from langchain_core.chat_history import BaseChatMessageHistory +# TODO: implement class ImageURL(TypedDict): """Funcchain type for passing an image as external url.""" url: str +# TODO: implement class ChatHistory(BaseChatMessageHistory): """Funcchain Type Wrapper for detecting ChatHistorys.""" From c98aab6e8f845adeb13b365706081f3b0ab96cbe Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:55 +0000 Subject: [PATCH 186/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20meta?= =?UTF-8?q?=5Finspect.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/meta_inspect.py | 105 --------------------------- 1 file changed, 105 deletions(-) delete mode 100644 src/funcchain/syntax/meta_inspect.py diff --git a/src/funcchain/syntax/meta_inspect.py b/src/funcchain/syntax/meta_inspect.py deleted file mode 100644 index d0fff3d..0000000 --- a/src/funcchain/syntax/meta_inspect.py +++ /dev/null @@ -1,105 +0,0 @@ -import types -from inspect import FrameInfo, currentframe, getouterframes -from typing import Any, Optional, Union - -from langchain_core.language_models import BaseChatModel -from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser, StrOutputParser - -from ..parser.parsers import ( - BoolOutputParser, - ParserBaseModel, - RetryPydanticOutputParser, -) - -FUNC_DEPTH = 4 - - -def get_parent_frame(depth: int = FUNC_DEPTH) -> FrameInfo: - """ - Get the dep'th parent function information. - """ - return getouterframes(currentframe())[depth] - - -def get_func_obj() -> types.FunctionType: - """ - Get the parent caller function. - """ - func_name = get_parent_frame().function - if func_name == "": - raise RuntimeError("Cannot get function object from module") - if func_name == "": - raise RuntimeError("Cannot get function object from lambda") - - try: - func = get_parent_frame().frame.f_globals[func_name] - except KeyError: - func = get_parent_frame(FUNC_DEPTH + 1).frame.f_locals[func_name] - return func - - -def from_docstring(f: Optional[types.FunctionType] = None) -> str: - """ - Get the docstring of the parent caller function. - """ - if doc_str := (f or get_func_obj()).__doc__: - return "\n".join([line.lstrip() for line in doc_str.split("\n")]) - raise ValueError(f"The funcchain ({get_parent_frame().function}) must have a docstring") - - -def get_output_type(f: Optional[types.FunctionType] = None) -> type: - """ - Get the output type annotation of the parent caller function. - """ - try: - return (f or get_func_obj()).__annotations__["return"] - except KeyError: - raise ValueError("The funcchain must have a return type annotation") - - -def parser_for( - output_type: type, - retry: int, - llm: BaseChatModel | str | None = None, -) -> BaseOutputParser | BaseGenerationOutputParser: - """ - Get the parser from the type annotation of the parent caller function. - """ - if isinstance(output_type, types.UnionType): - return None - # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) - if getattr(output_type, "__origin__", None) is Union: - output_type = output_type.__args__[0] # type: ignore - return None - # return MultiPydanticOutputParser(pydantic_objects=output_type.__args__) - if output_type is str: - return StrOutputParser() - if output_type is bool: - return BoolOutputParser() - if issubclass(output_type, ParserBaseModel): - return output_type.output_parser() # type: ignore - - from pydantic import BaseModel - - if issubclass(output_type, BaseModel): - return RetryPydanticOutputParser(pydantic_object=output_type, retry=retry, retry_llm=llm) - else: - raise SyntaxError(f"Output Type is not supported: {output_type}") - - -def kwargs_from_parent() -> dict[str, str]: - """ - Get the kwargs from the parent function. - """ - return get_parent_frame(FUNC_DEPTH - 1).frame.f_locals - - -def gather_signature(f: types.FunctionType) -> dict[str, Any]: - """ - Gather the signature of the parent caller function. - """ - return { - "instruction": from_docstring(f), - "input_args": list(f.__code__.co_varnames[: f.__code__.co_argcount]), - "output_type": get_output_type(f), - } From 4d6201e60224f2e3f72c4cee4a01776f642b63f6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:55 +0000 Subject: [PATCH 187/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20types.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/types.py | 88 ----------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 src/funcchain/syntax/types.py diff --git a/src/funcchain/syntax/types.py b/src/funcchain/syntax/types.py deleted file mode 100644 index cd71a53..0000000 --- a/src/funcchain/syntax/types.py +++ /dev/null @@ -1,88 +0,0 @@ -import json -import re -from typing import Optional - -from langchain_core.exceptions import OutputParserException -from langchain_core.output_parsers import BaseLLMOutputParser -from pydantic import BaseModel, Field -from typing_extensions import Self - - -class ParserBaseModel(BaseModel): - @classmethod - def output_parser(cls) -> BaseLLMOutputParser[Self]: - from ..parser.parsers import CustomPydanticOutputParser - - return CustomPydanticOutputParser(pydantic_object=cls) - - @classmethod - def parse(cls, text: str) -> Self: - """Override for custom parsing.""" - match = re.search(r"\{.*\}", text.strip(), re.MULTILINE | re.IGNORECASE | re.DOTALL) - json_str = "" - if match: - json_str = match.group() - json_object = json.loads(json_str, strict=False) - return cls.model_validate(json_object) - - @staticmethod - def format_instructions() -> str: - return ( - "Please respond with a json result matching the following schema:" - "\n\n```schema\n{schema}\n```\n" - "Do not repeat the schema. Only respond with the result." - ) - - @staticmethod - def custom_grammar() -> str | None: - return None - - -class CodeBlock(ParserBaseModel): - code: str - language: Optional[str] = None - - @classmethod - def parse(cls, text: str) -> "CodeBlock": - matches = re.finditer(r"```(?P\w+)?\n?(?P.*?)```", text, re.DOTALL) - for match in matches: - groupdict = match.groupdict() - groupdict["language"] = groupdict.get("language", None) - - # custom markdown fix - if groupdict["language"] == "markdown": - t = text.split("```markdown")[1] - return cls( - language="markdown", - code=t[: -(len(t.split("```")[-1]) + 3)], - ) - - return cls(**groupdict) - - return cls(code=text) # TODO: fix this hack - raise OutputParserException("Invalid codeblock") - - @staticmethod - def format_instructions() -> str: - return "Answer with a codeblock." - - @staticmethod - def custom_grammar() -> str | None: - return 'root ::= "```" ([^`] | "`" [^`] | "``" [^`])* "```"' - - def __str__(self) -> str: - return self.code - - -class Error(BaseModel): - """ - Fallback function for invalid input. - If you are unsure on what function to call, use this error function as fallback. - This will tell the user that the input is not valid. - """ - - title: str = Field(description="CamelCase Name titeling the error") - description: str = Field(..., description="Short description of the unexpected situation") - - def __raise__(self) -> None: - raise Exception(self.description) From 03bce745f1a3e3a33c6c3f268f2b495d2904bc02 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:55 +0000 Subject: [PATCH 188/451] =?UTF-8?q?=E2=9C=A8=20Add=20in-memory=20chat=20hi?= =?UTF-8?q?story?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/memory.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/funcchain/utils/memory.py diff --git a/src/funcchain/utils/memory.py b/src/funcchain/utils/memory.py new file mode 100644 index 0000000..cfe5d8e --- /dev/null +++ b/src/funcchain/utils/memory.py @@ -0,0 +1,23 @@ +"""langchain_community.chat_message_histories.in_memory.ChatMessageHistory""" + +from typing import List + +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.messages import BaseMessage +from langchain_core.pydantic_v1 import BaseModel, Field + + +class ChatMessageHistory(BaseChatMessageHistory, BaseModel): + """In memory implementation of chat message history. + + Stores messages in an in memory list. + """ + + messages: List[BaseMessage] = Field(default_factory=list) + + def add_message(self, message: BaseMessage) -> None: + """Add a self-created message to the store""" + self.messages.append(message) + + def clear(self) -> None: + self.messages = [] From c2572320dc595540fc59b512394e057d62e477f7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:55 +0000 Subject: [PATCH 189/451] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20Add=20image=20a?= =?UTF-8?q?nalysis=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ollama_test.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/ollama_test.py b/tests/ollama_test.py index f6733d1..acd7e5f 100644 --- a/tests/ollama_test.py +++ b/tests/ollama_test.py @@ -1,6 +1,7 @@ import pytest from funcchain import chain, settings -from pydantic import BaseModel +from PIL import Image +from pydantic import BaseModel, Field class Task(BaseModel): @@ -39,26 +40,28 @@ def test_neural_chat() -> None: ) -# def test_vision() -> None: -# from PIL import Image +class Analysis(BaseModel): + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") -# settings.llm = "mys/ggml_llava-v1.5-13b" -# class Analysis(BaseModel): -# description: str = Field(description="A description of the image") -# objects: list[str] = Field(description="A list of objects found in the image") +def analyse(image: Image.Image) -> Analysis: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + -# def analyse(image: Image.Image) -> Analysis: -# """ -# Analyse the image and extract its -# theme, description and objects. -# """ -# return chain() +@pytest.mark.skip_on_actions +def test_vision() -> None: + settings.llm = "ollama/bakllava" + + assert isinstance( + analyse(Image.open("examples/assets/old_chinese_temple.jpg")), + Analysis, + ) # todo check actual output -# assert isinstance( -# analyse(Image.open("examples/assets/old_chinese_temple.jpg")), -# Analysis, -# ) # TODO: Test union types # def test_union_types() -> None: From 0445a53513c588f5eadef8e4c2dd1843b225035b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 21:24:55 +0000 Subject: [PATCH 190/451] =?UTF-8?q?=E2=9C=A8=20Add=20examples=20test=20scr?= =?UTF-8?q?ipt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/run_examples_test.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/run_examples_test.py diff --git a/tests/run_examples_test.py b/tests/run_examples_test.py new file mode 100644 index 0000000..9eb75f0 --- /dev/null +++ b/tests/run_examples_test.py @@ -0,0 +1,31 @@ +import asyncio +import glob +import subprocess + + +async def run_script(file_path: str) -> tuple[str, int | None, bytes, bytes]: + """Run a single script and return the result.""" + process = await asyncio.create_subprocess_exec("python", file_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = await process.communicate() + return file_path, process.returncode, stdout, stderr + + +async def main() -> None: + files: list[str] = glob.glob("example/**/*.py", recursive=True) + tasks: list = [run_script(file) for file in files] + results: list[tuple[str, int | None, bytes, bytes]] = await asyncio.gather(*tasks) + + for file, returncode, stdout, stderr in results: + if returncode != 0: + print(f"Error in {file}:") + print(stderr.decode()) + else: + print(f"{file} executed successfully.") + + +def test_examples() -> None: + asyncio.run(main()) + + +if __name__ == "__main__": + test_examples() From aa267f4782d20a5d3f51a36634c009cedf7ce3e9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 191/451] =?UTF-8?q?=F0=9F=94=A7=20Enable=20console=20strea?= =?UTF-8?q?ming=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/chatgpt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/chatgpt.py b/examples/chatgpt.py index c1a34ea..aa09320 100644 --- a/examples/chatgpt.py +++ b/examples/chatgpt.py @@ -5,6 +5,7 @@ from funcchain.utils.memory import ChatMessageHistory settings.llm = "openai/gpt-4" +settings.console_stream = True history = ChatMessageHistory() From 05281735d43ef2a581d7fa5c71848d69532fbd7a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 192/451] =?UTF-8?q?=F0=9F=94=A5=20Remove=20settings=20depe?= =?UTF-8?q?ndency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/enums.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/enums.py b/examples/enums.py index d562960..46dcb48 100644 --- a/examples/enums.py +++ b/examples/enums.py @@ -1,6 +1,6 @@ from enum import Enum -from funcchain import chain, settings +from funcchain import chain from pydantic import BaseModel @@ -21,6 +21,4 @@ def make_decision(question: str) -> Decision: if __name__ == "__main__": - settings.llm = "ollama/phi-2" - print(make_decision("Do you like apples?")) From 1732eca9a390c6009db3208e779f075199ab914b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 193/451] =?UTF-8?q?=F0=9F=94=8D=20Clarify=20exception=20co?= =?UTF-8?q?mment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/error_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/error_output.py b/examples/error_output.py index d805f86..92d77e7 100644 --- a/examples/error_output.py +++ b/examples/error_output.py @@ -10,7 +10,7 @@ class User(BaseModel): def extract_user_info(text: str) -> User | Error: """ Extract the user information from the given text. - If you do not have enough infos, raise. + In case you do not have enough infos, raise. """ return chain() From f08bc1caa4636ca3d4c9718d2db8686f03f6c252 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 194/451] =?UTF-8?q?=F0=9F=94=A7=20Disable=20default=20logg?= =?UTF-8?q?er=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pydantic_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pydantic_validation.py b/examples/pydantic_validation.py index 133d1bb..cfc104e 100644 --- a/examples/pydantic_validation.py +++ b/examples/pydantic_validation.py @@ -1,7 +1,7 @@ from funcchain import chain, settings from pydantic import BaseModel, field_validator -settings.llm = "ollama/openchat" +# settings.llm = "ollama/openchat" settings.console_stream = True From 630546bf8ad8b8fa17a57e6cc6edb2e429e43520 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 195/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20stream=5Frunnab?= =?UTF-8?q?les.py=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/stream_runnables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/stream_runnables.py b/examples/stream_runnables.py index 1b592e5..b7e0050 100644 --- a/examples/stream_runnables.py +++ b/examples/stream_runnables.py @@ -1,6 +1,6 @@ from typing import AsyncIterator, Iterator -from funcchain import chain, settings +from funcchain import chain from funcchain.syntax import runnable from funcchain.syntax.components import RouterChat from funcchain.syntax.components.handler import BasicChatHandler @@ -85,7 +85,7 @@ async def aconvert_to_ai_message(input: AsyncIterator[list[str]]) -> AsyncIterat def main() -> None: - for chunk in chat.stream(HumanMessage(content="Hey whatsup?")): + for chunk in chat.stream(HumanMessage(content="Hey whatsup?"), config={"configurable": {"session_id": ""}}): if isinstance(chunk, AIMessage): print(chunk.content, flush=True) if isinstance(chunk, str): From 2f7800a21cb0f7b6922ea0e57278255c692890c0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 196/451] =?UTF-8?q?=F0=9F=94=84=20Switch=20vision=20model?= =?UTF-8?q?=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/vision.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/vision.py b/examples/vision.py index a73b580..cae56bd 100644 --- a/examples/vision.py +++ b/examples/vision.py @@ -2,8 +2,8 @@ from PIL import Image from pydantic import BaseModel, Field -# settings.llm = "openai/gpt-4-vision-preview" -settings.llm = "ollama/bakllava" +settings.llm = "openai/gpt-4-vision-preview" +# settings.llm = "ollama/bakllava" settings.console_stream = True From 678e47529ecb3de36b245316cec844f6ed6ddabf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 197/451] =?UTF-8?q?=E2=9C=A8=20Allow=20UnionType=20for=20o?= =?UTF-8?q?utput.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index b1739c5..06b68ac 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -86,7 +86,7 @@ def patch_openai_function_to_pydantic( def create_chain( system: str, instruction: str, - output_type: Type[ChainOutput], + output_type: Type[ChainOutput], # | UnionType, context: list[BaseMessage], memory: BaseChatMessageHistory, settings: FuncchainSettings, From 70c7f721f01d169b1acc698d33fb96d934fbe309 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 198/451] =?UTF-8?q?=E2=9C=A8=20Implement=20args=5Ffrom=5Fp?= =?UTF-8?q?arent=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/meta_inspect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/backend/meta_inspect.py b/src/funcchain/backend/meta_inspect.py index 5b909ad..51b8518 100644 --- a/src/funcchain/backend/meta_inspect.py +++ b/src/funcchain/backend/meta_inspect.py @@ -1,6 +1,6 @@ import types from inspect import FrameInfo, currentframe, getouterframes -from typing import Any, Optional +from typing import Optional FUNC_DEPTH = 4 @@ -59,7 +59,7 @@ def args_from_parent() -> list[tuple[str, type]]: """ Get input args with type hints from parent function """ - # TODO: implement + return [(arg, t) for arg, t in get_func_obj().__annotations__.items() if arg != "return" and arg != "self"] def gather_signature( From 2e412b2bbffcf82de54434229777ec0ffc1f01fa Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 199/451] =?UTF-8?q?=E2=9C=A8=20Add=20parse=20method=20stub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/json_schema.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/funcchain/parser/json_schema.py b/src/funcchain/parser/json_schema.py index 3da00c1..12c1f80 100644 --- a/src/funcchain/parser/json_schema.py +++ b/src/funcchain/parser/json_schema.py @@ -87,4 +87,5 @@ def retry_chain(self) -> Runnable: class RetryJsonPydanticUnionParser(BaseOutputParser[M]): - ... + def parse(self, text: str) -> M: + raise NotImplementedError From 6bf58964cd09c2bb3de390c02c2cf27b3872eaef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 200/451] =?UTF-8?q?=F0=9F=94=87=20Remove=20debug=20print?= =?UTF-8?q?=20statement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/openai_functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/funcchain/parser/openai_functions.py b/src/funcchain/parser/openai_functions.py index 910eaba..f2dd8c5 100644 --- a/src/funcchain/parser/openai_functions.py +++ b/src/funcchain/parser/openai_functions.py @@ -62,8 +62,6 @@ def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: if function_call["name"] not in output_type_names: raise OutputParserException("Invalid function call") - print(function_call["name"]) - output_type = self._get_output_type(function_call["name"]) generation = result[0] From 26bee8aec650bdbc8193510f361216800dc8e38c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 201/451] =?UTF-8?q?=F0=9F=94=84=20Update=20Union=20parser?= =?UTF-8?q?=20instantiation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/selector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/parser/selector.py b/src/funcchain/parser/selector.py index e72dd85..02e76d9 100644 --- a/src/funcchain/parser/selector.py +++ b/src/funcchain/parser/selector.py @@ -18,8 +18,8 @@ def parser_for( Get the parser from the type annotation of the parent caller function. """ if isinstance(output_type, types.UnionType) or getattr(output_type, "__origin__", None) is Union: - output_type = output_type.__args__[0] # type: ignore - return RetryJsonPydanticUnionParser(pydantic_objects=output_type.__args__) # type: ignore # TODO: fix this + # output_type = output_type.__args__[0] # type: ignore + return RetryJsonPydanticUnionParser() # type: ignore # TODO: fix this if output_type is str: return StrOutputParser() if output_type is bool: From da14b32012cc43a240f3daab78d6d9ab92794d66 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 22 Jan 2024 22:05:16 +0000 Subject: [PATCH 202/451] =?UTF-8?q?=E2=9C=A8=20Support=20union=20output=20?= =?UTF-8?q?types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py index 62bc832..5e1cb7f 100644 --- a/src/funcchain/schema/signature.py +++ b/src/funcchain/schema/signature.py @@ -1,3 +1,4 @@ +from types import UnionType from typing import Any, Generic, TypeVar from langchain_core.messages import BaseMessage @@ -27,7 +28,7 @@ class Signature(BaseModel, Generic[T]): # -> e.g. Callbacks adds custom callbacks # -> e.g. SystemMessage adds a system message - output_type: type[T] + output_type: type[T] | UnionType """ Type to parse the output into. """ # todo: is history really needed? maybe this could be a background optimization From 08f91047f148049cb23345e73e0c89a18c16eb63 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:05:19 +0000 Subject: [PATCH 203/451] =?UTF-8?q?=E2=8F=AB=20update=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 9 +++-- requirements-dev.lock | 94 +++++++++++++++++++++---------------------- requirements.lock | 27 ++++++------- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1f101c8..2805db7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,13 +6,12 @@ authors = [ { name = "Shroominic", email = "contact@shroominic.com" } ] dependencies = [ - "langchain_core>=0.1.10", - "langchain_openai>=0.0.2.post1", # TODO: make optional + "langchain_core>=0.1", + "langchain_openai>=0.0.3", "pydantic-settings>=2", "docstring-parser>=0.15", "rich>=13", "jinja2>=3", - "pillow>=10", # TODO: make optional ] license = "MIT" readme = "README.md" @@ -59,9 +58,13 @@ openai = [ ollama = [ "langchain_community", ] +pillow = [ + "pillow", +] all = [ "funcchain[openai]", "funcchain[ollama]", + "funcchain[pillow]", "langchain", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index db438bb..e6702b0 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,104 +7,104 @@ # all-features: false -e file:. -aiohttp==3.9.0 +aiohttp==3.9.1 aiosignal==1.3.1 annotated-types==0.6.0 -anyio==3.7.1 +anyio==4.2.0 asttokens==2.4.1 -attrs==23.1.0 -babel==2.13.1 -beautifulsoup4==4.12.2 +attrs==23.2.0 +babel==2.14.0 +beautifulsoup4==4.12.3 certifi==2023.11.17 cfgv==3.4.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 -dataclasses-json==0.6.2 +dataclasses-json==0.6.3 decorator==5.1.1 -distlib==0.3.7 -distro==1.8.0 +distlib==0.3.8 +distro==1.9.0 docstring-parser==0.15 executing==2.0.1 faiss-cpu==1.7.4 filelock==3.13.1 -frozenlist==1.4.0 +frozenlist==1.4.1 ghp-import==2.1.0 h11==0.14.0 httpcore==1.0.2 -httpx==0.25.1 -identify==2.5.32 -idna==3.4 +httpx==0.26.0 +identify==2.5.33 +idna==3.6 iniconfig==2.0.0 -ipython==8.18.1 +ipython==8.20.0 isort==5.13.2 jedi==0.19.1 -jinja2==3.1.2 +jinja2==3.1.3 jsonpatch==1.33 jsonpointer==2.4 -langchain==0.1.0 -langchain-community==0.0.12 -langchain-core==0.1.10 -langchain-openai==0.0.2.post1 -langsmith==0.0.80 -markdown==3.5.1 +langchain==0.1.3 +langchain-community==0.0.15 +langchain-core==0.1.15 +langchain-openai==0.0.3 +langsmith==0.0.83 +markdown==3.5.2 markdown-it-py==3.0.0 -markupsafe==2.1.3 -marshmallow==3.20.1 +markupsafe==2.1.4 +marshmallow==3.20.2 matplotlib-inline==0.1.6 mdurl==0.1.2 mergedeep==1.3.4 mkdocs==1.5.3 -mkdocs-material==9.4.10 -mkdocs-material-extensions==1.3 +mkdocs-material==9.5.5 +mkdocs-material-extensions==1.3.1 multidict==6.0.4 -mypy==1.7.0 +mypy==1.8.0 mypy-extensions==1.0.0 nodeenv==1.8.0 -numpy==1.26.2 -openai==1.7.2 +numpy==1.26.3 +openai==1.9.0 packaging==23.2 paginate==0.5.6 parso==0.8.3 -pathspec==0.11.2 +pathspec==0.12.1 pexpect==4.9.0 -pillow==10.1.0 -platformdirs==4.0.0 -pluggy==1.3.0 -pre-commit==3.5.0 -prompt-toolkit==3.0.41 +pillow==10.2.0 +platformdirs==4.1.0 +pluggy==1.4.0 +pre-commit==3.6.0 +prompt-toolkit==3.0.43 ptyprocess==0.7.0 pure-eval==0.2.2 -pydantic==2.5.2 -pydantic-core==2.14.5 +pydantic==2.5.3 +pydantic-core==2.14.6 pydantic-settings==2.1.0 pygments==2.17.2 -pymdown-extensions==10.4 -pytest==7.4.3 +pymdown-extensions==10.7 +pytest==7.4.4 python-dateutil==2.8.2 -python-dotenv==1.0.0 +python-dotenv==1.0.1 pyyaml==6.0.1 pyyaml-env-tag==0.1 -regex==2023.10.3 +regex==2023.12.25 requests==2.31.0 rich==13.7.0 -ruff==0.1.6 +ruff==0.1.14 six==1.16.0 sniffio==1.3.0 soupsieve==2.5 -sqlalchemy==2.0.23 +sqlalchemy==2.0.25 stack-data==0.6.3 tenacity==8.2.3 tiktoken==0.5.2 tqdm==4.66.1 -traitlets==5.14.0 +traitlets==5.14.1 types-pyyaml==6.0.12.12 -typing-extensions==4.8.0 +typing-extensions==4.9.0 typing-inspect==0.9.0 urllib3==2.1.0 -virtualenv==20.24.7 +virtualenv==20.25.0 watchdog==3.0.0 -wcwidth==0.2.12 -yarl==1.9.3 +wcwidth==0.2.13 +yarl==1.9.4 # The following packages are considered to be unsafe in a requirements file: -setuptools==69.0.2 +setuptools==69.0.3 diff --git a/requirements.lock b/requirements.lock index 14c4468..8a0cccd 100644 --- a/requirements.lock +++ b/requirements.lock @@ -8,7 +8,7 @@ -e file:. annotated-types==0.6.0 -anyio==3.7.1 +anyio==4.2.0 certifi==2023.11.17 charset-normalizer==3.3.2 distro==1.9.0 @@ -16,25 +16,24 @@ docstring-parser==0.15 h11==0.14.0 httpcore==1.0.2 httpx==0.26.0 -idna==3.4 -jinja2==3.1.2 +idna==3.6 +jinja2==3.1.3 jsonpatch==1.33 jsonpointer==2.4 -langchain-core==0.1.10 -langchain-openai==0.0.2.post1 -langsmith==0.0.80 +langchain-core==0.1.15 +langchain-openai==0.0.3 +langsmith==0.0.83 markdown-it-py==3.0.0 -markupsafe==2.1.3 +markupsafe==2.1.4 mdurl==0.1.2 -numpy==1.26.2 -openai==1.7.2 +numpy==1.26.3 +openai==1.9.0 packaging==23.2 -pillow==10.1.0 -pydantic==2.5.2 -pydantic-core==2.14.5 +pydantic==2.5.3 +pydantic-core==2.14.6 pydantic-settings==2.1.0 pygments==2.17.2 -python-dotenv==1.0.0 +python-dotenv==1.0.1 pyyaml==6.0.1 regex==2023.12.25 requests==2.31.0 @@ -43,5 +42,5 @@ sniffio==1.3.0 tenacity==8.2.3 tiktoken==0.5.2 tqdm==4.66.1 -typing-extensions==4.8.0 +typing-extensions==4.9.0 urllib3==2.1.0 From 75fe422fd32350e0885397f51b25374c2ff05605 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:14 +0000 Subject: [PATCH 204/451] =?UTF-8?q?=F0=9F=94=84=20Update=20Image=20import?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 666fdec..69934cd 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ match lst: ## Vision Models ```python -from PIL import Image +from funcchain import Image from pydantic import BaseModel, Field from funcchain import chain, settings From bd754c8b3b61cd10ac4286917bc7941af97dc0af Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:14 +0000 Subject: [PATCH 205/451] =?UTF-8?q?=F0=9F=94=A7=20Correct=20output=5Ftypes?= =?UTF-8?q?=20tuple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/dynamic_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dynamic_router.py b/examples/dynamic_router.py index 80a4ee8..7fb371b 100644 --- a/examples/dynamic_router.py +++ b/examples/dynamic_router.py @@ -34,7 +34,7 @@ class RouterModel(BaseModel): route_query = compile_runnable( instruction="Given the user query select the best query handler for it.", input_args=["user_query", "query_handlers"], - output_type=RouterModel, + output_types=(RouterModel,), ) selected_route = route_query.invoke( From 6a5a073efa885ebf75d2414b6455538ed0a7d75a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:14 +0000 Subject: [PATCH 206/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20fruits?= =?UTF-8?q?alad.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/fruitsalad.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 examples/fruitsalad.py diff --git a/examples/fruitsalad.py b/examples/fruitsalad.py deleted file mode 100644 index 4dac483..0000000 --- a/examples/fruitsalad.py +++ /dev/null @@ -1,19 +0,0 @@ -from funcchain import chain -from pydantic import BaseModel - - -class FruitSalad(BaseModel): - bananas: int = 0 - apples: int = 0 - - -def sum_fruits(fruit_salad: FruitSalad) -> int: - """ - Sum the number of fruits in a fruit salad. - """ - return chain() - - -def test_fruit_salad() -> None: - fruit_salad = FruitSalad(bananas=3, apples=5) - assert sum_fruits(fruit_salad) == 8 From 4b5affa8c17e60391ed6a8d5bb1928b93e381f0e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:14 +0000 Subject: [PATCH 207/451] =?UTF-8?q?=F0=9F=8D=8C=20Add=20fruit=20salad=20su?= =?UTF-8?q?m=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/openai_json_mode.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/examples/openai_json_mode.py b/examples/openai_json_mode.py index e69de29..ed4c1db 100644 --- a/examples/openai_json_mode.py +++ b/examples/openai_json_mode.py @@ -0,0 +1,25 @@ +from funcchain import chain, settings +from pydantic import BaseModel + +settings.console_stream = True + + +class FruitSalad(BaseModel): + bananas: int = 0 + apples: int = 0 + + +class Result(BaseModel): + sum: int + + +def sum_fruits(fruit_salad: FruitSalad) -> Result: + """ + Sum the number of fruits in a fruit salad. + """ + return chain() + + +if __name__ == "__main__": + fruit_salad = FruitSalad(bananas=3, apples=5) + assert sum_fruits(fruit_salad) == 8 From 6ab32bfacaad5a45d498b9babdabb49a8498369a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:14 +0000 Subject: [PATCH 208/451] =?UTF-8?q?=F0=9F=94=A7=20Disable=20default=20LLM?= =?UTF-8?q?=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/simple/gather_infos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple/gather_infos.py b/examples/simple/gather_infos.py index 2e833ca..d8234f1 100644 --- a/examples/simple/gather_infos.py +++ b/examples/simple/gather_infos.py @@ -1,7 +1,7 @@ from funcchain import chain, settings from pydantic import BaseModel -settings.llm = "ollama/openchat" +# settings.llm = "ollama/openchat" settings.console_stream = True From 540944f184a2e76463c8171ec9de4dc53281be25 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:14 +0000 Subject: [PATCH 209/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20image=20impor?= =?UTF-8?q?t=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/vision.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/vision.py b/examples/vision.py index cae56bd..cf2881d 100644 --- a/examples/vision.py +++ b/examples/vision.py @@ -1,5 +1,4 @@ -from funcchain import chain, settings -from PIL import Image +from funcchain import Image, chain, settings from pydantic import BaseModel, Field settings.llm = "openai/gpt-4-vision-preview" @@ -15,7 +14,7 @@ class AnalysisResult(BaseModel): objects: list[str] = Field(description="A list of objects found in the image") -def analyse_image(image: Image.Image) -> AnalysisResult: +def analyse_image(image: Image) -> AnalysisResult: """ Analyse the image and extract its theme, description and objects. @@ -24,7 +23,7 @@ def analyse_image(image: Image.Image) -> AnalysisResult: if __name__ == "__main__": - example_image = Image.open("examples/assets/old_chinese_temple.jpg") + example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") result = analyse_image(example_image) From 16ce8bf3d73a0a3e3fd6c3efa987ead427b564d7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 210/451] =?UTF-8?q?=F0=9F=94=BC=20Bump=20version=20to=20al?= =?UTF-8?q?pha.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2805db7..ef8b3b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcchain" -version = "0.2.0-alpha.3" +version = "0.2.0-alpha.4" description = "🔖 write prompts as python functions" authors = [ { name = "Shroominic", email = "contact@shroominic.com" } From e2ec0008b885dcc4fc3e960be9c62e3bc5224cf7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 211/451] =?UTF-8?q?=F0=9F=93=A6=20Add=20Image=20input=20ty?= =?UTF-8?q?pe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/funcchain/__init__.py b/src/funcchain/__init__.py index ac08f48..86c6cd5 100644 --- a/src/funcchain/__init__.py +++ b/src/funcchain/__init__.py @@ -2,6 +2,7 @@ from .backend.settings import settings from .syntax.executable import achain, chain +from .syntax.input_types import Image from .syntax.output_types import Error __all__ = [ @@ -9,6 +10,7 @@ "chain", "achain", "BaseModel", + "Image", "Error", "runnable", ] From abe390537231f422f9465526c73276891813fa46 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 212/451] =?UTF-8?q?=E2=9C=A8=20Refactor=20type=20handling,?= =?UTF-8?q?=20parsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 72 ++++++++++++++++++------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index 06b68ac..58fe52f 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -1,5 +1,4 @@ -from types import UnionType -from typing import Type, TypeVar +from typing import Any, TypeVar from langchain_core.callbacks import Callbacks from langchain_core.chat_history import BaseChatMessageHistory @@ -7,15 +6,15 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser from langchain_core.runnables import Runnable -from PIL import Image from pydantic import BaseModel from ..model.abilities import is_openai_function_model, is_vision_model from ..model.defaults import univeral_model_selector -from ..parser.openai_functions import OpenAIFunctionPydanticParser, OpenAIFunctionPydanticUnionParser +from ..parser.openai_functions import RetryOpenAIFunctionPydanticParser, RetryOpenAIFunctionPydanticUnionParser from ..parser.schema_converter import pydantic_to_grammar from ..parser.selector import parser_for from ..schema.signature import Signature +from ..syntax.input_types import Image from ..syntax.output_types import ParserBaseModel from ..utils.msg_tools import msg_to_str from ..utils.pydantic import multi_pydantic_to_functions, pydantic_to_functions @@ -33,28 +32,28 @@ # TODO: do patch instead of seperate creation def create_union_chain( - output_type: UnionType, + output_types: tuple[type], instruction_prompt: HumanImageMessagePromptTemplate, system: str, memory: BaseChatMessageHistory, context: list[BaseMessage], llm: BaseChatModel, - input_kwargs: dict[str, str], -) -> Runnable[dict[str, str], BaseModel]: + input_kwargs: dict[str, Any], +) -> Runnable[dict[str, str], Any]: """ Compile a langchain runnable chain from the funcchain syntax. """ - if not all(issubclass(t, BaseModel) for t in output_type.__args__): + if not all(issubclass(t, BaseModel) for t in output_types): raise RuntimeError("Funcchain union types are currently only supported for pydantic models.") - output_types: list[Type[BaseModel]] = output_type.__args__ # type: ignore output_type_names = [t.__name__ for t in output_types] input_kwargs["format_instructions"] = f"Extract to one of these output types: {output_type_names}." functions = multi_pydantic_to_functions(output_types) - llm = llm.bind(**functions) # type: ignore + _llm = llm + llm = _llm.bind(**functions) # type: ignore prompt = create_chat_prompt( system, @@ -67,7 +66,7 @@ def create_union_chain( memory=memory, ) - return prompt | llm | OpenAIFunctionPydanticUnionParser(output_types=output_types) + return prompt | llm | RetryOpenAIFunctionPydanticUnionParser(output_types=output_types, retry=3, retry_llm=_llm) def patch_openai_function_to_pydantic( @@ -78,19 +77,21 @@ def patch_openai_function_to_pydantic( input_kwargs["format_instructions"] = f"Extract to {output_type.__name__}." functions = pydantic_to_functions(output_type) + _llm = llm llm = llm.bind(**functions) # type: ignore - return llm, OpenAIFunctionPydanticParser(pydantic_schema=output_type) + return llm, RetryOpenAIFunctionPydanticParser(pydantic_schema=output_type, retry=3, retry_llm=_llm) def create_chain( system: str, instruction: str, - output_type: Type[ChainOutput], # | UnionType, + output_types: tuple[type[ChainOutput]], context: list[BaseMessage], memory: BaseChatMessageHistory, settings: FuncchainSettings, input_args: list[tuple[str, type]], + temp_images: list[Image] = [], ) -> Runnable[dict[str, str], ChainOutput]: """ Compile a langchain runnable chain from the funcchain syntax. @@ -99,7 +100,7 @@ def create_chain( _llm = _gather_llm(settings) llm = _add_custom_callbacks(_llm, settings) - parser = parser_for(output_type, retry=settings.retry_parse, llm=llm) + parser = parser_for(output_types, retry=settings.retry_parse, llm=llm) # handle input arguments prompt_args: list[str] = [] @@ -109,12 +110,13 @@ def create_chain( for i in input_args: if i[1] is str: prompt_args.append(i[0]) - if i[1] is BaseModel: + if issubclass(i[1], BaseModel): pydantic_args.append(i[0]) else: special_args.append(i) - input_kwargs = {k: "" for k in prompt_args + pydantic_args} + # TODO: change this into input_args + input_kwargs = {k: "" for k in (prompt_args + pydantic_args)} # add format instructions for parser f_instructions = None @@ -139,6 +141,7 @@ def create_chain( # for vision models images = _handle_images(llm, memory, input_kwargs) + images.extend(temp_images) # create prompts instruction_prompt = create_instruction_prompt( @@ -149,16 +152,17 @@ def create_chain( ) chat_prompt = create_chat_prompt(system, instruction_prompt, context, memory) - # add formatted instruction to chat history - memory.add_message(instruction_prompt.format(**input_kwargs)) + # TODO: think why this was needed + # # add formatted instruction to chat history + # memory.add_message(instruction_prompt.format(**input_kwargs)) - _inject_grammar_for_local_models(llm, output_type) + _inject_grammar_for_local_models(llm, output_types) # function model patches if is_openai_function_model(llm): - if isinstance(output_type, UnionType): + if len(output_types) > 1: return create_union_chain( - output_type, + output_types, instruction_prompt, system, memory, @@ -166,7 +170,7 @@ def create_chain( llm, input_kwargs, ) - + output_type = output_types[0] if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): if settings.streaming and hasattr(llm, "model_kwargs"): llm.model_kwargs = {"response_format": {"type": "json_object"}} @@ -177,7 +181,9 @@ def create_chain( return chat_prompt | llm | parser -def compile_chain(signature: Signature[ChainOutput]) -> Runnable[dict[str, str], ChainOutput]: +def compile_chain( + signature: Signature[ChainOutput], temp_images: list[Image] = [] +) -> Runnable[dict[str, str], ChainOutput]: """ Compile a signature to a runnable chain. """ @@ -195,11 +201,12 @@ def compile_chain(signature: Signature[ChainOutput]) -> Runnable[dict[str, str], return create_chain( msg_to_str(system), signature.instruction, - signature.output_type, + signature.output_types, signature.history, memory, signature.settings, signature.input_args, + temp_images, ) @@ -243,15 +250,15 @@ def _crop_large_inputs( def _handle_images( llm: BaseChatModel, memory: BaseChatMessageHistory, - input_kwargs: dict[str, str], -) -> list[Image.Image]: + input_kwargs: dict[str, Any], +) -> list[Image]: """ Handle images for vision models. """ - images = [v for v in input_kwargs.values() if isinstance(v, Image.Image)] + images = [v for v in input_kwargs.values() if isinstance(v, Image)] if is_vision_model(llm): for k in list(input_kwargs.keys()): - if isinstance(input_kwargs[k], Image.Image): + if isinstance(input_kwargs[k], Image): del input_kwargs[k] elif images: raise RuntimeError("Images as input are only supported for vision models.") @@ -262,7 +269,10 @@ def _handle_images( return images -def _inject_grammar_for_local_models(llm: BaseChatModel, output_type: type) -> None: +def _inject_grammar_for_local_models( + llm: BaseChatModel, + output_types: tuple[type], +) -> None: """ Inject GBNF grammar into local models. """ @@ -272,9 +282,9 @@ def _inject_grammar_for_local_models(llm: BaseChatModel, output_type: type) -> N pass else: if isinstance(llm, ChatOllama): - if isinstance(output_type, UnionType): + if len(output_types) > 1: raise NotImplementedError("Union types are not yet supported for LlamaCpp models.") # TODO: implement - + output_type = output_types[0] if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): llm.grammar = pydantic_to_grammar(output_type) if issubclass(output_type, ParserBaseModel): From fd0c848bee06e8fbe5c5967216dc4e7d54b8a693 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 213/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20type=20handli?= =?UTF-8?q?ng=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/meta_inspect.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/funcchain/backend/meta_inspect.py b/src/funcchain/backend/meta_inspect.py index 51b8518..0266dd1 100644 --- a/src/funcchain/backend/meta_inspect.py +++ b/src/funcchain/backend/meta_inspect.py @@ -1,5 +1,5 @@ -import types from inspect import FrameInfo, currentframe, getouterframes +from types import FunctionType, UnionType from typing import Optional FUNC_DEPTH = 4 @@ -12,7 +12,7 @@ def get_parent_frame(depth: int = FUNC_DEPTH) -> FrameInfo: return getouterframes(currentframe())[depth] -def get_func_obj() -> types.FunctionType: +def get_func_obj() -> FunctionType: """ Get the parent caller function. """ @@ -29,7 +29,7 @@ def get_func_obj() -> types.FunctionType: return func -def from_docstring(f: Optional[types.FunctionType] = None) -> str: +def from_docstring(f: Optional[FunctionType] = None) -> str: """ Get the docstring of the parent caller function. """ @@ -38,12 +38,17 @@ def from_docstring(f: Optional[types.FunctionType] = None) -> str: raise ValueError(f"The funcchain ({get_parent_frame().function}) must have a docstring") -def get_output_type(f: Optional[types.FunctionType] = None) -> type: +def get_output_types(f: Optional[FunctionType] = None) -> tuple[type]: """ Get the output type annotation of the parent caller function. - """ + Returns a list of types in case of a union, otherwise a list with one type. + """ # TODO: implement union type lists try: - return (f or get_func_obj()).__annotations__["return"] + return_type = (f or get_func_obj()).__annotations__["return"] + if isinstance(return_type, UnionType): + return return_type.__args__ # type: ignore + else: + return (return_type,) except KeyError: raise ValueError("The funcchain must have a return type annotation") @@ -63,13 +68,13 @@ def args_from_parent() -> list[tuple[str, type]]: def gather_signature( - f: types.FunctionType, -) -> dict[str, str | list[tuple[str, type]] | type]: + f: FunctionType, +) -> dict[str, str | list[tuple[str, type]] | tuple[type]]: """ Gather the signature of the parent caller function. """ return { "instruction": from_docstring(f), "input_args": [(arg, f.__annotations__[arg]) for arg in f.__code__.co_varnames[: f.__code__.co_argcount]], - "output_type": get_output_type(f), + "output_types": get_output_types(f), } From 9a3431d42232311627810b0f2aaa08a02d2f4d6d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 214/451] =?UTF-8?q?=F0=9F=93=B8=20Update=20image=20handlin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/prompt.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/funcchain/backend/prompt.py b/src/funcchain/backend/prompt.py index 148b8df..266554f 100644 --- a/src/funcchain/backend/prompt.py +++ b/src/funcchain/backend/prompt.py @@ -9,15 +9,14 @@ MessagePromptTemplateT, ) from langchain_core.prompts.prompt import PromptTemplate -from PIL import Image from pydantic import BaseModel -from ..utils.image import image_to_base64_url +from ..syntax.input_types import Image def create_instruction_prompt( instruction: str, - images: list[Image.Image], + images: list[Image], input_kwargs: dict[str, Any], format_instructions: Optional[str] = None, ) -> "HumanImageMessagePromptTemplate": @@ -28,15 +27,16 @@ def create_instruction_prompt( _filter_fstring_vars(input_kwargs) inject_vars = [f"{var.upper()}:\n{{{var}}}\n" for var, _ in input_kwargs.items() if var not in required_f_str_vars] + added_instruction = "\n".join(inject_vars) instruction = added_instruction + instruction - images = [image_to_base64_url(image) for image in images] + _images = [image.url for image in images] return HumanImageMessagePromptTemplate.from_template( template=instruction, template_format=template_format, - images=images, + images=_images, partial_variables={"format_instructions": format_instructions} if format_instructions else None, ) @@ -45,7 +45,7 @@ def create_chat_prompt( system: str, instruction_template: "HumanImageMessagePromptTemplate", context: list[BaseMessage], - memory: BaseChatMessageHistory, + memory: BaseChatMessageHistory, # TODO: remove and do memory placeholder ) -> ChatPromptTemplate: """ Compose a chat prompt from a system message, @@ -55,7 +55,9 @@ def create_chat_prompt( if system and memory.messages and isinstance(memory.messages[0], SystemMessage): memory.messages.pop(0) + # TODO: fix union type problem if memory.messages and isinstance(memory.messages[-1], HumanMessage): + print("specialchatprompt") return ChatPromptTemplate.from_messages( [ *([SystemMessage(content=system)] if system else []), @@ -63,7 +65,6 @@ def create_chat_prompt( *context, ] ) - return ChatPromptTemplate.from_messages( [ *([SystemMessage(content=system)] if system else []), From ad1746c2cf7937caf76883b4f49083c8eaa0911d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 215/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20output=5Ftypes?= =?UTF-8?q?=20annotation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/json_schema.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/funcchain/parser/json_schema.py b/src/funcchain/parser/json_schema.py index 12c1f80..05b36be 100644 --- a/src/funcchain/parser/json_schema.py +++ b/src/funcchain/parser/json_schema.py @@ -80,12 +80,16 @@ def retry_chain(self) -> Runnable: return compile_runnable( instruction="Retry parsing the output by fixing the error.", input_args=["output", "error"], - output_type=self.pydantic_object, + output_types=(self.pydantic_object,), llm=self.retry_llm, settings_override={"retry_parse": self.retry - 1}, ) class RetryJsonPydanticUnionParser(BaseOutputParser[M]): + """Parse an output using a pydantic model.""" + + output_types: list[Type[M]] + def parse(self, text: str) -> M: raise NotImplementedError From 13480686cf1bd5984c0ed155a14c0e6b46322f2f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 216/451] =?UTF-8?q?=E2=9C=A8=20Add=20retry=20logic=20parse?= =?UTF-8?q?rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/openai_functions.py | 153 ++++++++++++++--------- 1 file changed, 91 insertions(+), 62 deletions(-) diff --git a/src/funcchain/parser/openai_functions.py b/src/funcchain/parser/openai_functions.py index f2dd8c5..834174c 100644 --- a/src/funcchain/parser/openai_functions.py +++ b/src/funcchain/parser/openai_functions.py @@ -2,8 +2,10 @@ from typing import Type, TypeVar from langchain_core.exceptions import OutputParserException +from langchain_core.language_models import BaseChatModel from langchain_core.output_parsers import BaseGenerationOutputParser from langchain_core.outputs import ChatGeneration, Generation +from langchain_core.runnables import Runnable from pydantic import BaseModel, ValidationError from ..syntax.output_types import CodeBlock as CodeBlock @@ -12,86 +14,101 @@ M = TypeVar("M", bound=BaseModel) -# TODO: retry wrapper -class OpenAIFunctionPydanticParser(BaseGenerationOutputParser[M]): +class RetryOpenAIFunctionPydanticParser(BaseGenerationOutputParser[M]): pydantic_schema: Type[M] args_only: bool = False + retry: int + retry_llm: BaseChatModel | str | None = None def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException( - "This output parser can only be used with a chat generation.", - ) - message = generation.message try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException( + "This output parser can only be used with a chat generation.", + ) + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) - if self.args_only: - _result = func_call["arguments"] - else: - _result = func_call - try: if self.args_only: - pydantic_args = self.pydantic_schema.model_validate_json(_result) + pydantic_args = self.pydantic_schema.model_validate_json(func_call) else: - pydantic_args = self.pydantic_schema.model_validate_json(_result["arguments"]) - except ValidationError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) - return pydantic_args - - -# TODO: retry wrapper -class OpenAIFunctionPydanticUnionParser(BaseGenerationOutputParser[M]): - output_types: list[Type[M]] + pydantic_args = self.pydantic_schema.model_validate_json(func_call["arguments"]) + + return pydantic_args + except ValidationError as e: + if self.retry > 0: + print(f"Retrying parsing {self.pydantic_schema.__name__}...") + return self.retry_chain.invoke( + input={"output": result, "error": str(e)}, + config={"run_name": "RetryOpenAIFunctionPydanticParser"}, + ) + # no retries left + raise OutputParserException(str(e), llm_output=msg_to_str(message)) + + @property + def retry_chain(self) -> Runnable: + from ..syntax.executable import compile_runnable + + return compile_runnable( + instruction="Retry parsing the output by fixing the error.", + input_args=["output", "error"], + output_types=(self.pydantic_schema,), + llm=self.retry_llm, + settings_override={"retry_parse": self.retry - 1}, + ) + + +class RetryOpenAIFunctionPydanticUnionParser(BaseGenerationOutputParser[M]): + output_types: tuple[Type[M]] args_only: bool = False + retry: int + retry_llm: BaseChatModel | str | None = None def parse_result(self, result: list[Generation], *, partial: bool = False) -> M: - function_call = self._pre_parse_function_call(result) - - output_type_names = [t.__name__.lower() for t in self.output_types] + try: + function_call = self._pre_parse_function_call(result) - if function_call["name"] not in output_type_names: - raise OutputParserException("Invalid function call") + output_type_names = [t.__name__.lower() for t in self.output_types] - output_type = self._get_output_type(function_call["name"]) + if function_call["name"] not in output_type_names: + raise OutputParserException("Invalid function call") - generation = result[0] - if not isinstance(generation, ChatGeneration): - raise OutputParserException("This output parser can only be used with a chat generation.") - message = generation.message - try: - func_call = copy.deepcopy(message.additional_kwargs["function_call"]) - except KeyError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) + output_type = self._get_output_type(function_call["name"]) - if self.args_only: - _result = func_call["arguments"] - else: - _result = func_call + generation = result[0] + if not isinstance(generation, ChatGeneration): + raise OutputParserException("This output parser can only be used with a chat generation.") + message = generation.message + try: + func_call = copy.deepcopy(message.additional_kwargs["function_call"]) + except KeyError as exc: + raise OutputParserException( + f"Could not parse function call: {exc}", + llm_output=msg_to_str(message), + ) - try: if self.args_only: - pydantic_args = output_type.model_validate_json(_result) + pydantic_args = output_type.model_validate_json(func_call["arguments"]) else: - pydantic_args = output_type.model_validate_json(_result["arguments"]) - except ValidationError as exc: - raise OutputParserException( - f"Could not parse function call: {exc}", - llm_output=msg_to_str(message), - ) - return pydantic_args + pydantic_args = output_type.model_validate_json(func_call["arguments"]) + + return pydantic_args + except (ValidationError, OutputParserException) as e: + if self.retry > 0: + print(f"Retrying parsing {output_type.__name__}...") + return self.retry_chain.invoke( + input={"output": result, "error": str(e)}, + config={"run_name": "RetryOpenAIFunctionPydanticUnionParser"}, + ) + # no retries left + raise OutputParserException(str(e), llm_output=msg_to_str(message)) def _pre_parse_function_call(self, result: list[Generation]) -> dict: generation = result[0] @@ -113,3 +130,15 @@ def _get_output_type(self, function_name: str) -> Type[M]: if output_type_iter is None: raise OutputParserException(f"No parser found for function: {function_name}") return next(output_type_iter) + + @property + def retry_chain(self) -> Runnable: + from ..syntax.executable import compile_runnable + + return compile_runnable( + instruction="Retry parsing the output by fixing the error.", + input_args=["output", "error"], + output_types=self.output_types, + llm=self.retry_llm, + settings_override={"retry_parse": self.retry - 1}, + ) From aa41e504c90ad0d2ff6a8e8207ad43f4f9a33ca8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 217/451] =?UTF-8?q?=E2=9C=A8=20Refactor=20parser=20selecti?= =?UTF-8?q?on=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/selector.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/funcchain/parser/selector.py b/src/funcchain/parser/selector.py index 02e76d9..f6a8809 100644 --- a/src/funcchain/parser/selector.py +++ b/src/funcchain/parser/selector.py @@ -1,8 +1,6 @@ -import types -from typing import Union - from langchain_core.language_models import BaseChatModel from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser, StrOutputParser +from pydantic import BaseModel from ..parser.json_schema import RetryJsonPydanticParser, RetryJsonPydanticUnionParser from ..parser.parsers import BoolOutputParser @@ -10,26 +8,29 @@ def parser_for( - output_type: type, + output_types: tuple[type], retry: int, llm: BaseChatModel | str | None = None, ) -> BaseOutputParser | BaseGenerationOutputParser: """ Get the parser from the type annotation of the parent caller function. """ - if isinstance(output_type, types.UnionType) or getattr(output_type, "__origin__", None) is Union: - # output_type = output_type.__args__[0] # type: ignore - return RetryJsonPydanticUnionParser() # type: ignore # TODO: fix this + if len(output_types) > 1: + return RetryJsonPydanticUnionParser(output_types=output_types) + + output_type = output_types[0] + if output_type is str: return StrOutputParser() + if output_type is bool: return BoolOutputParser() + if issubclass(output_type, ParserBaseModel): return output_type.output_parser() # type: ignore - from pydantic import BaseModel - if issubclass(output_type, BaseModel): return RetryJsonPydanticParser(pydantic_object=output_type, retry=retry, retry_llm=llm) + else: raise SyntaxError(f"Output Type is not supported: {output_type}") From 801d54a166bab3dbc6c7aa798e2e4f096b2f9b4b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 218/451] =?UTF-8?q?=F0=9F=94=84=20Update=20output=20type?= =?UTF-8?q?=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py index 5e1cb7f..12e0c80 100644 --- a/src/funcchain/schema/signature.py +++ b/src/funcchain/schema/signature.py @@ -1,4 +1,3 @@ -from types import UnionType from typing import Any, Generic, TypeVar from langchain_core.messages import BaseMessage @@ -28,7 +27,7 @@ class Signature(BaseModel, Generic[T]): # -> e.g. Callbacks adds custom callbacks # -> e.g. SystemMessage adds a system message - output_type: type[T] | UnionType + output_types: tuple[type[T]] """ Type to parse the output into. """ # todo: is history really needed? maybe this could be a background optimization @@ -53,7 +52,7 @@ def __hash__(self) -> int: ( self.instruction, tuple(self.input_args), - self.output_type, + tuple(self.output_types), tuple(self.history), self.settings, ) From 491abfd70f75fdf6a13ab40ed1e0ee50d2ba03ae Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 219/451] =?UTF-8?q?=F0=9F=94=A7=20Adjust=20output=20type?= =?UTF-8?q?=20tuples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/router.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/syntax/components/router.py b/src/funcchain/syntax/components/router.py index d58b291..3d344bf 100644 --- a/src/funcchain/syntax/components/router.py +++ b/src/funcchain/syntax/components/router.py @@ -79,7 +79,7 @@ class RouterModel(BaseModel): return compile_runnable( instruction="Given the user request select the appropriate route.", input_args=["user_request", "routes"], # todo: optional images - output_type=RouterModel, + output_types=(RouterModel,), context=self.history.messages if self.history else [], llm=self.llm, ) @@ -92,7 +92,7 @@ def _add_default_handler(self) -> None: | compile_runnable( instruction="{user_request}", input_args=["user_request"], - output_type=str, + output_types=(str,), llm=self.llm, ) | RunnableLambda(lambda x: AIMessage(content=x)) From aa9ec6c6af31c270cd4b37568add26f2c9c33857 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 220/451] =?UTF-8?q?=E2=9C=A8=20Add=20type=20hinting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/syntax/decorators.py b/src/funcchain/syntax/decorators.py index ea92487..180fe0c 100644 --- a/src/funcchain/syntax/decorators.py +++ b/src/funcchain/syntax/decorators.py @@ -50,7 +50,7 @@ def decorator(f: Callable) -> Runnable: _signature["settings"] = create_local_settings(override=settings) _signature["auto_tune"] = auto_tune - sig = Signature(**_signature) + sig: Signature = Signature(**_signature) return compile_chain(sig) if callable(f): From 7ad094c5aaa461c5a556f11ddcda46c182354ce8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 221/451] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20Support=20image?= =?UTF-8?q?=20inputs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 42 +++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py index 06ce656..48eb5bc 100644 --- a/src/funcchain/syntax/executable.py +++ b/src/funcchain/syntax/executable.py @@ -10,29 +10,31 @@ from ..backend.meta_inspect import ( args_from_parent, from_docstring, - get_output_type, + get_output_types, get_parent_frame, kwargs_from_parent, ) from ..backend.settings import SettingsOverride, create_local_settings from ..schema.signature import Signature from ..utils.memory import ChatMessageHistory +from .input_types import Image def chain( + *, system: str | None = None, instruction: str | None = None, context: list[BaseMessage] = [], memory: BaseChatMessageHistory | None = None, settings_override: SettingsOverride = {}, - **input_kwargs: str, + **input_kwargs: Any, ) -> Any: """ Generate response of llm for provided instructions. """ settings = create_local_settings(settings_override) callbacks: Callbacks = None - output_type = get_output_type() + output_types = get_output_types() input_args: list[tuple[str, type]] = args_from_parent() memory = memory or ChatMessageHistory() @@ -42,14 +44,21 @@ def chain( system = system or settings.system_prompt instruction = instruction or from_docstring() + # temp image handling + temp_images: list[Image] = [] + for k, v in input_kwargs.copy().items(): + if isinstance(v, Image): + temp_images.append(v) + input_kwargs.pop(k) + sig: Signature = Signature( instruction=instruction, input_args=input_args, - output_type=output_type, + output_types=output_types, history=context, settings=settings, ) - chain: Runnable[dict[str, str], Any] = compile_chain(sig) + chain: Runnable[dict[str, Any], Any] = compile_chain(sig, temp_images) result = chain.invoke(input_kwargs, {"run_name": get_parent_frame(3).function, "callbacks": callbacks}) if memory and isinstance(result, str): @@ -60,19 +69,20 @@ def chain( async def achain( + *, system: str | None = None, instruction: str | None = None, context: list[BaseMessage] = [], memory: BaseChatMessageHistory | None = None, settings_override: SettingsOverride = {}, - **input_kwargs: str, + **input_kwargs: Any, ) -> Any: """ Asyncronously generate response of llm for provided instructions. """ settings = create_local_settings(settings_override) callbacks: Callbacks = None - output_type = get_output_type() + output_types = get_output_types() input_args: list[tuple[str, type]] = args_from_parent() memory = memory or ChatMessageHistory() @@ -82,14 +92,21 @@ async def achain( system = system or settings.system_prompt instruction = instruction or from_docstring() + # temp image handling + temp_images: list[Image] = [] + for v, k in input_kwargs.copy().items(): + if isinstance(v, Image): + temp_images.append(v) + input_kwargs.pop(k) + sig: Signature = Signature( instruction=instruction, input_args=input_args, - output_type=output_type, + output_types=output_types, history=context, settings=settings, ) - chain: Runnable[dict[str, str], Any] = compile_chain(sig) + chain: Runnable[dict[str, str], Any] = compile_chain(sig, temp_images) result = await chain.ainvoke(input_kwargs, {"run_name": get_parent_frame(5).function, "callbacks": callbacks}) if memory and isinstance(result, str): @@ -103,8 +120,9 @@ async def achain( def compile_runnable( + *, instruction: str, - output_type: type[ChainOut], + output_types: tuple[type[ChainOut]], input_args: list[str] = [], context: list = [], llm: BaseChatModel | str | None = None, @@ -124,9 +142,9 @@ def compile_runnable( sig: Signature = Signature( instruction=instruction, input_args=_input_args, - output_type=output_type, + output_types=output_types, history=context, settings=settings, ) - return compile_chain(sig) + return compile_chain(sig, temp_images=[]) From e1a8a1bff93afe5f65e832a1cd3fc1aa0d857a1f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 222/451] =?UTF-8?q?=E2=9C=A8=20Add=20Image=20class=20suppo?= =?UTF-8?q?rt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/input_types.py | 75 +++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/src/funcchain/syntax/input_types.py b/src/funcchain/syntax/input_types.py index be7374d..9833718 100644 --- a/src/funcchain/syntax/input_types.py +++ b/src/funcchain/syntax/input_types.py @@ -1,13 +1,78 @@ -from typing import TypedDict +import base64 +from typing import TYPE_CHECKING from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.messages import BaseMessage +from ..utils.msg_tools import msg_images -# TODO: implement -class ImageURL(TypedDict): - """Funcchain type for passing an image as external url.""" +if TYPE_CHECKING: + from PIL.Image import Image as PImage +else: + PImage = type("PImage") + + +class Image: + """ + Funcchain type for passing an image. + Supports multiple input and output formats. + (base64, bytes, pillow, file, web_url) + """ + + __slots__ = ("url",) + + def __init__(self, base64_url: str) -> None: + self.url = base64_url + + def from_bytes(self, data: bytes) -> "Image": + encoded_string = base64.b64encode(data).decode() + return self.from_base64(encoded_string) + + @classmethod + def from_message(cls, message: BaseMessage) -> list["Image"]: + return [cls(i) for i in images] if (images := msg_images(message)) else [] + + @classmethod + def from_base64(cls, base64: str) -> "Image": + return cls("data:image/png;base64," + base64) + + @classmethod + def from_file(cls, path: str) -> "Image": + with open(path, "rb") as file: + encoded_string = base64.b64encode(file.read()).decode() + return cls("data:image/png;base64," + encoded_string) + + @classmethod + def from_pillow(cls, image: PImage) -> "Image": + encoded_string = base64.b64encode(image.tobytes()).decode() + return cls("data:image/png;base64," + encoded_string) + + @classmethod + def from_url(cls, url: str) -> "Image": + from requests import get # type: ignore + + response_content = get(url).content + encoded_string = base64.b64encode(response_content).decode() + return cls("data:image/png;base64," + encoded_string) + + def to_base64(self) -> str: + return self.url.split(",")[1] + + def to_bytes(self) -> bytes: + base64_str = self.to_base64() + return base64.b64decode(base64_str) + + def to_pillow(self) -> PImage: + from io import BytesIO # type: ignore + + image_bytes = self.to_bytes() + return PImage.open(BytesIO(image_bytes)) + + def to_file(self, path: str) -> None: + open(path, "wb").write(self.to_bytes()) - url: str + def __str__(self) -> str: + return self.url # TODO: implement From 8bdc31f3944934b6f1e3784169887bb34e82415f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 223/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20image=20utili?= =?UTF-8?q?ty=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/image.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/funcchain/utils/image.py b/src/funcchain/utils/image.py index b4b5371..38f55ea 100644 --- a/src/funcchain/utils/image.py +++ b/src/funcchain/utils/image.py @@ -1,18 +1,36 @@ +from __future__ import annotations + from base64 import b64decode, b64encode from io import BytesIO +from typing import TYPE_CHECKING + +from ..syntax.input_types import Image + +if TYPE_CHECKING: + from PIL.Image import Image as PImage +else: + PImage = type("PImage") + -from PIL import Image +def image_to_base64_url(image: Image) -> str: + return image.url -def image_to_base64_url(image: Image.Image) -> str: +def base64_url_to_image(base64_url: str) -> Image: + return Image(base64_url) + + +def pillow_image_to_base64_url(image: PImage) -> str: with BytesIO() as output: image.save(output, format="PNG") base64_image = b64encode(output.getvalue()).decode("utf-8") return f"data:image/jpeg;base64,{base64_image}" -def base64_url_to_image(base64_url: str) -> Image.Image: +def base64_url_to_pillow_image(base64_url: str) -> PImage: + from PIL.Image import Image as PImage + base64_image = base64_url.split(",")[1] image_bytes = b64decode(base64_image) - image = Image.open(BytesIO(image_bytes)) + image = PImage.open(BytesIO(image_bytes)) return image From c9bd424070e3a9896a3527bf3e12d50005023054 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 224/451] =?UTF-8?q?=F0=9F=94=A7=20Change=20list=20to=20tup?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/pydantic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/utils/pydantic.py b/src/funcchain/utils/pydantic.py index fc8f089..855ecfc 100644 --- a/src/funcchain/utils/pydantic.py +++ b/src/funcchain/utils/pydantic.py @@ -54,7 +54,7 @@ def pydantic_to_functions(pydantic_type: Type[BaseModel]) -> dict[str, Any]: def multi_pydantic_to_functions( - pydantic_types: list[Type[BaseModel]], + pydantic_types: tuple[Type[BaseModel]], ) -> dict[str, Any]: functions: list[dict[str, Any]] = [ pydantic_to_functions(pydantic_type)["functions"][0] for pydantic_type in pydantic_types From a4241272b88760ebcd1a9a0fd2a1d6de94aae023 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 225/451] =?UTF-8?q?=F0=9F=94=A7=20Adjust=20answer=20parame?= =?UTF-8?q?ter=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/async_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/async_test.py b/tests/async_test.py index 5f69e81..d5e3830 100644 --- a/tests/async_test.py +++ b/tests/async_test.py @@ -23,7 +23,7 @@ class RankedAnswer(BaseModel): async def rank_answers( question: str, - answers: list[tuple[int, str]], + answers: str, ) -> RankedAnswer: """ Given the list of answers, select the answer @@ -39,7 +39,7 @@ async def expert_answer( # Shuffle the answers to ensure randomness enum_answers = list(enumerate(answers)) shuffle(enum_answers) - ranked_answers = await gather(*(rank_answers(question, enum_answers) for _ in range(3))) + ranked_answers = await gather(*(rank_answers(question, str(enum_answers)) for _ in range(3))) highest_ranked_answer = max( ranked_answers, key=lambda x: sum(1 for ans in ranked_answers if ans.selected_answer == x.selected_answer), From dc9a8de4f7e5a85c62006413ebc4d568a31f686f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 226/451] =?UTF-8?q?=F0=9F=94=84=20Refactor=20image=20analy?= =?UTF-8?q?sis=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ollama_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ollama_test.py b/tests/ollama_test.py index acd7e5f..624723e 100644 --- a/tests/ollama_test.py +++ b/tests/ollama_test.py @@ -1,6 +1,5 @@ import pytest -from funcchain import chain, settings -from PIL import Image +from funcchain import Image, chain, settings from pydantic import BaseModel, Field @@ -45,7 +44,7 @@ class Analysis(BaseModel): objects: list[str] = Field(description="A list of objects found in the image") -def analyse(image: Image.Image) -> Analysis: +def analyse(image: Image) -> Analysis: """ Analyse the image and extract its theme, description and objects. @@ -56,9 +55,10 @@ def analyse(image: Image.Image) -> Analysis: @pytest.mark.skip_on_actions def test_vision() -> None: settings.llm = "ollama/bakllava" + settings.console_stream = True assert isinstance( - analyse(Image.open("examples/assets/old_chinese_temple.jpg")), + analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), Analysis, ) # todo check actual output From d31455d3dfb3f8efffeadf1a304ae1523495b4ef Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:09:15 +0000 Subject: [PATCH 227/451] =?UTF-8?q?=F0=9F=94=84=20Update=20image=20import?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/openai_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/openai_test.py b/tests/openai_test.py index 9644427..efbabed 100644 --- a/tests/openai_test.py +++ b/tests/openai_test.py @@ -37,7 +37,7 @@ def test_gpt4() -> None: def test_vision() -> None: - from PIL import Image + from funcchain import Image settings.llm = "openai/gpt-4-vision-preview" @@ -45,7 +45,7 @@ class Analysis(BaseModel): description: str = Field(description="A description of the image") objects: list[str] = Field(description="A list of objects found in the image") - def analyse(image: Image.Image) -> Analysis: + def analyse(image: Image) -> Analysis: """ Analyse the image and extract its theme, description and objects. @@ -53,7 +53,7 @@ def analyse(image: Image.Image) -> Analysis: return chain() assert isinstance( - analyse(Image.open("examples/assets/old_chinese_temple.jpg")), + analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), Analysis, ) From 276cb336c4ce1c86d9d651f04e25a59d0c34bc96 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:10:15 +0000 Subject: [PATCH 228/451] =?UTF-8?q?=F0=9F=94=A7=20Remove=20console=5Fstrea?= =?UTF-8?q?m=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ollama_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ollama_test.py b/tests/ollama_test.py index 624723e..1cf0546 100644 --- a/tests/ollama_test.py +++ b/tests/ollama_test.py @@ -55,7 +55,6 @@ def analyse(image: Image) -> Analysis: @pytest.mark.skip_on_actions def test_vision() -> None: settings.llm = "ollama/bakllava" - settings.console_stream = True assert isinstance( analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), From 4aabde856cf93fa78bdd299c4cc738d0668200e3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:28:37 +0000 Subject: [PATCH 229/451] =?UTF-8?q?=F0=9F=94=A7=20fix=20pre-commit=20issue?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dspy.todo | 2 +- src/funcchain/model/__init__.py | 1 - src/funcchain/parser/json_schema.py | 2 +- src/funcchain/syntax/components/handler.py | 4 +++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dspy.todo b/dspy.todo index 5e11116..4e64b62 100644 --- a/dspy.todo +++ b/dspy.todo @@ -4,6 +4,6 @@ To make this possible I need to: - unify syntax and schemas - invent new syntax for special dspy modules (COT, FewShot, ...) - seperate string and structured types logic - - + - booth are cutting edge ml libraries diff --git a/src/funcchain/model/__init__.py b/src/funcchain/model/__init__.py index 8b13789..e69de29 100644 --- a/src/funcchain/model/__init__.py +++ b/src/funcchain/model/__init__.py @@ -1 +0,0 @@ - diff --git a/src/funcchain/parser/json_schema.py b/src/funcchain/parser/json_schema.py index 05b36be..a193b21 100644 --- a/src/funcchain/parser/json_schema.py +++ b/src/funcchain/parser/json_schema.py @@ -2,7 +2,7 @@ import re from typing import Type, TypeVar -import yaml +import yaml # type: ignore from langchain_core.exceptions import OutputParserException from langchain_core.language_models import BaseChatModel from langchain_core.output_parsers import BaseOutputParser diff --git a/src/funcchain/syntax/components/handler.py b/src/funcchain/syntax/components/handler.py index ae75231..e6e8ef5 100644 --- a/src/funcchain/syntax/components/handler.py +++ b/src/funcchain/syntax/components/handler.py @@ -1,3 +1,5 @@ +from typing import Union + from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.language_models import BaseChatModel from langchain_core.messages import AIMessage, HumanMessage @@ -9,7 +11,7 @@ from ...model.defaults import univeral_model_selector from ...utils.msg_tools import msg_to_str -UniversalLLM = BaseChatModel | str | None +UniversalLLM = Union[BaseChatModel, str, None] def load_universal_llm(llm: UniversalLLM) -> BaseChatModel: From 44957aa546e6330d3b325332b3f265859025a7df Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 25 Jan 2024 12:29:54 +0000 Subject: [PATCH 230/451] =?UTF-8?q?=F0=9F=94=A7=20fix=20empty=20system=20m?= =?UTF-8?q?essage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index 58fe52f..ade8e7c 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -188,11 +188,8 @@ def compile_chain( Compile a signature to a runnable chain. """ system = ( - [msg for msg in signature.history if isinstance(msg, SystemMessage)] - or [ - SystemMessage(content=""), - ] - ).pop() + [msg for msg in signature.history if isinstance(msg, SystemMessage)] or [None] # type: ignore + )[0] from ..utils.memory import ChatMessageHistory From 3d16c8b915672e358f6d9d4a71c97d475720f16d Mon Sep 17 00:00:00 2001 From: luckysanpedro Date: Thu, 25 Jan 2024 13:34:21 +0100 Subject: [PATCH 231/451] Documentation --- README.md | 9 +- docs/css/custom.css | 27 ++ docs/css/termynal.css | 109 ++++++ docs/examples.md | 115 +++++++ docs/examples/chat.md | 126 +++++++ docs/examples/dynamic_router.md | 239 +++++++++++++ docs/examples/enums.md | 92 +++++ docs/examples/error_output.md | 91 +++++ docs/examples/literals.md | 88 +++++ docs/getting-started/installation.md | 11 + docs/getting-started/introduction.md | 480 +++++++++++++++++++++++++++ docs/index.md | 45 +-- docs/js/custom.js | 113 +++++++ docs/js/termynal.js | 263 +++++++++++++++ mkdocs.yml | 71 +++- requirements-dev.lock | 3 +- requirements.lock | 1 + 17 files changed, 1856 insertions(+), 27 deletions(-) create mode 100644 docs/css/custom.css create mode 100644 docs/css/termynal.css create mode 100644 docs/examples/chat.md create mode 100644 docs/examples/dynamic_router.md create mode 100644 docs/examples/enums.md create mode 100644 docs/examples/error_output.md create mode 100644 docs/examples/literals.md create mode 100644 docs/getting-started/introduction.md create mode 100644 docs/js/custom.js create mode 100644 docs/js/termynal.js diff --git a/README.md b/README.md index 666fdec..38a5856 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://docs.pydantic.dev/latest/contributing/#badges) [![Twitter Follow](https://img.shields.io/twitter/follow/shroominic?style=social)](https://x.com/shroominic) -```bash -> pip install funcchain -``` - +
+ ```bash + $ > pip install funcchain + ``` +
## Introduction `funcchain` is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. diff --git a/docs/css/custom.css b/docs/css/custom.css new file mode 100644 index 0000000..6b76f33 --- /dev/null +++ b/docs/css/custom.css @@ -0,0 +1,27 @@ +.termynal-comment { + color: #4a968f; + font-style: italic; + display: block; +} + +.termy [data-termynal] { + white-space: pre-wrap; +} + +a.external-link::after { + /* \00A0 is a non-breaking space + to make the mark be on the same line as the link + */ + content: "\00A0[↪]"; +} + +a.internal-link::after { + /* \00A0 is a non-breaking space + to make the mark be on the same line as the link + */ + content: "\00A0↪"; +} + +.shadow { + box-shadow: 5px 5px 10px #999; +} \ No newline at end of file diff --git a/docs/css/termynal.css b/docs/css/termynal.css new file mode 100644 index 0000000..8938c97 --- /dev/null +++ b/docs/css/termynal.css @@ -0,0 +1,109 @@ +/** + * termynal.js + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + + :root { + --color-bg: #252a33; + --color-text: #eee; + --color-text-subtle: #a2a2a2; +} + +[data-termynal] { + width: 750px; + max-width: 100%; + background: var(--color-bg); + color: var(--color-text); + /* font-size: 18px; */ + font-size: 15px; + /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ + font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + border-radius: 4px; + padding: 75px 45px 35px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +[data-termynal]:before { + content: ''; + position: absolute; + top: 15px; + left: 15px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: 50%; + /* A little hack to display the window buttons in one pseudo element. */ + background: #d9515d; + -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; + box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; +} + +[data-termynal]:after { + content: 'bash'; + position: absolute; + color: var(--color-text-subtle); + top: 5px; + left: 0; + width: 100%; + text-align: center; +} + +a[data-terminal-control] { + text-align: right; + display: block; + color: #aebbff; +} + +[data-ty] { + display: block; + line-height: 2; +} + +[data-ty]:before { + /* Set up defaults and ensure empty lines are displayed. */ + content: ''; + display: inline-block; + vertical-align: middle; +} + +[data-ty="input"]:before, +[data-ty-prompt]:before { + margin-right: 0.75em; + color: var(--color-text-subtle); +} + +[data-ty="input"]:before { + content: '$'; +} + +[data-ty][data-ty-prompt]:before { + content: attr(data-ty-prompt); +} + +[data-ty-cursor]:after { + content: attr(data-ty-cursor); + font-family: monospace; + margin-left: 0.5em; + -webkit-animation: blink 1s infinite; + animation: blink 1s infinite; +} + + +/* Cursor animation */ + +@-webkit-keyframes blink { + 50% { + opacity: 0; + } +} + +@keyframes blink { + 50% { + opacity: 0; + } +} \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index 6ee0a62..d35e109 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -65,3 +65,118 @@ The funcchain project makes it really simple to leverage large language models i ## Advanced Examples For advanced examples, checkout the examples directory [here](https://github.com/shroominic/funcchain/tree/main/examples) + +## Simple chatgpt rebuild with memory/history. +!!! Example + chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) + + + +```python +from funcchain import chain, settings +from funcchain.utils.memory import ChatMessageHistory + +settings.llm = "openai/gpt-4" +settings.console_stream = True + +history = ChatMessageHistory() + + +def ask(question: str) -> str: + return chain( + system="You are an advanced AI Assistant.", + instruction=question, + memory=history, + ) + + +def chat_loop() -> None: + while True: + query = input("> ") + + if query == "exit": + break + + if query == "clear": + global history + history.clear() + print("\033c") + continue + + ask(query) + + +if __name__ == "__main__": + print("Hey! How can I help you?\n") + chat_loop() +``` + + + +
+ ```terminal + initial print function: + $ Hey! How can I help you? + $ > + + userprompt: + $ > Say that Funcchain is cool + + assistant terminal asnwer: + $ Funcchain is cool. + ``` +
+ +## Instructions + +Import nececary funcchain components + +```python +from funcchain import chain, settings +from funcchain.utils.memory import ChatMessageHistory +``` +# +Settings +```python +settings.llm = "openai/gpt-4" +settings.console_stream = True +``` +!!! Options + Funcchain supports multiple LLMs and has the ability to stream received LLM text instead of waiting for the complete answer. For configuration options, see below: + + ```markdown + - `settings.llm`: Specify the language model to use. See MODELS.md for available options. + - Streaming: Set `settings.console_stream` to `True` to enable streaming, + or `False` to disable it. + ``` + + [MODELS.md]([MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md)) + +# +Establish a chat history + +```python +history = ChatMessageHistory() +``` +Stores messages in an in memory list. This will crate a thread of messages. + +See [memory.py] //Todo: Insert Link + +# +Ask function explained +```python +def ask(question: str) -> str: +return chain( +system="You are an advanced AI Assistant.", +instruction=question, +memory=history, +) +``` + +This function sends a question to the Funcchain chain function. + +It sets the system context as an advanced AI Assistant and passes the question as an instruction. + +The history object is used to maintain a thread of messages for context. + +The function returns the response from the chain function. diff --git a/docs/examples/chat.md b/docs/examples/chat.md new file mode 100644 index 0000000..9a37200 --- /dev/null +++ b/docs/examples/chat.md @@ -0,0 +1,126 @@ +## Simple chatgpt rebuild with memory/history. +!!! Example + chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) + +!!! Important + Ensure you have set up your API key for the LLM of your choice, or Funcchain will look for a `.env` file. So in `.env` set up your key. + ```python + OPENAI_API_KEY="sk-XXX" + ``` + + +## Code Example + +```python +from funcchain import chain, settings +from funcchain.utils.memory import ChatMessageHistory + +settings.llm = "openai/gpt-4" +settings.console_stream = True + +history = ChatMessageHistory() + + +def ask(question: str) -> str: + return chain( + system="You are an advanced AI Assistant.", + instruction=question, + memory=history, + ) + + +def chat_loop() -> None: + while True: + query = input("> ") + + if query == "exit": + break + + if query == "clear": + global history + history.clear() + print("\033c") + continue + + ask(query) + + +if __name__ == "__main__": + print("Hey! How can I help you?\n") + chat_loop() +``` + + + +
+ ```terminal + initial print function: + $ Hey! How can I help you? + $ > + + userprompt: + $ > Say that Funcchain is cool + + assistant terminal asnwer: + $ Funcchain is cool. + ``` +
+ +## Instructions + +!!! Step-by-Step + **Import nececary funcchain components** + + ```python + from funcchain import chain, settings + from funcchain.utils.memory import ChatMessageHistory + ``` + + **Settings** + + ```python + settings.llm = "openai/gpt-4" + settings.console_stream = True + ``` + + + Funcchain supports multiple LLMs and has the ability to stream received LLM text instead of waiting for the complete answer. For configuration options, see below: + + ```markdown + - `settings.llm`: Specify the language model to use. See MODELS.md for available options. + - Streaming: Set `settings.console_stream` to `True` to enable streaming, + or `False` to disable it. + ``` + + [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) + + + **Establish a chat history** + + ```python + history = ChatMessageHistory() + ``` + Stores messages in an in memory list. This will crate a thread of messages. + + See [memory.py] //Todo: Insert Link + + + **Define ask function** + See how funcchain uses `chain()` with an input `str` to return an output of type `str` + + ```python + def ask(question: str) -> str: + return chain( + system="You are an advanced AI Assistant.", + instruction=question, + memory=history, + ) + ``` + + This function sends a question to the Funcchain `chain()` function. + + It sets the system context as an advanced AI Assistant and passes the question as an instruction. + + The history object is used to maintain a thread of messages for context. + + The function returns the response from the chain function. diff --git a/docs/examples/dynamic_router.md b/docs/examples/dynamic_router.md new file mode 100644 index 0000000..696190e --- /dev/null +++ b/docs/examples/dynamic_router.md @@ -0,0 +1,239 @@ +# Dynamic Chat Router with Funcchain + +!!! Example + dynamic_router.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/dynamic_router.py) + +In this example we will use funcchain to build a LLM routing pipeline. +This is a very useful LLM task and can be used in a variety of applications. +You can abstract this for your own usage. +This should serve as an example of how to archive complex structures using funcchain. + +A dynamic chat router that selects the appropriate handler for user queries based on predefined routes. + +## Full Code Example + +```python +from enum import Enum +from typing import Any, Callable, TypedDict + +from funcchain.syntax.executable import compile_runnable +from pydantic import BaseModel, Field + +# Dynamic Router Definition: + + +class Route(TypedDict): + handler: Callable + description: str + + +class DynamicChatRouter(BaseModel): + routes: dict[str, Route] + + def _routes_repr(self) -> str: + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) + + def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) + + class RouterModel(BaseModel): + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) + + route_query = compile_runnable( + instruction="Given the user query select the best query handler for it.", + input_args=["user_query", "query_handlers"], + output_type=RouterModel, + ) + + selected_route = route_query.invoke( + input={ + "user_query": user_query, + "query_handlers": self._routes_repr(), + } + ).selector + assert isinstance(selected_route, str) + + return self.routes[selected_route]["handler"](user_query, **kwargs) + + +# Example Usage: + + +def handle_pdf_requests(user_query: str) -> str: + return "Handling PDF requests with user query: " + user_query + + +def handle_csv_requests(user_query: str) -> str: + return "Handling CSV requests with user query: " + user_query + + +def handle_default_requests(user_query: str) -> str: + return "Handling DEFAULT requests with user query: " + user_query + + +router = DynamicChatRouter( + routes={ + "pdf": { + "handler": handle_pdf_requests, + "description": "Call this for requests including PDF Files.", + }, + "csv": { + "handler": handle_csv_requests, + "description": "Call this for requests including CSV Files.", + }, + "default": { + "handler": handle_default_requests, + "description": "Call this for all other requests.", + }, + }, +) + + +router.invoke_route("Can you summarize this csv?") +``` + +Demo +
+```python +User: +$ Can you summarize this csv? +$ ............... +Handling CSV requests with user query: Can you summarize this csv? +``` +
+ +## Instructions + +!!! Step-by-Step + + **Nececary imports** + ``` + from enum import Enum + from typing import Any, Callable, TypedDict + + from funcchain.syntax.executable import compile_runnable + from pydantic import BaseModel, Field + ``` + + **Define Route Type** + ```python + class Route(TypedDict): + handler: Callable + description: str + ``` + + Create a `TypedDict` to define the structure of a route with a handler function and a description. Just leave this unchanged if not intentionally experimenting. + + **Implement Route Representation** + Establish a Router class + ```python + class DynamicChatRouter(BaseModel): + routes: dict[str, Route] + ``` + + **_routes_repr():** + Returns a string representation of all routes and their descriptions, used to help the language model understand the available routes. + + ```python + def _routes_repr(self) -> str: + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) + ``` + + **invoke_route(user_query: str, **kwargs: Any) -> Any: ** + This method takes a user query and additional keyword arguments. Inside invoke_route, an Enum named RouteChoices is dynamically created with keys corresponding to the route names. This Enum is used to validate the selected route. + ```python + def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) + ``` + + **Compile the Route Selection Logic** + The `RouterModel` class in this example is used for defining the expected output structure that the `compile_runnable` function will use to determine the best route for a given user query. + + + ```python + class RouterModel(BaseModel): + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) + + route_query = compile_runnable( + instruction="Given the user query select the best query handler for it.", + input_args=["user_query", "query_handlers"], + output_type=RouterModel, + ) + + selected_route = route_query.invoke( + input={ + "user_query": user_query, + "query_handlers": self._routes_repr(), + } + ).selector + assert isinstance(selected_route, str) + + return self.routes[selected_route]["handler"](user_query, **kwargs) + ``` + + - `RouterModel`: Holds the route selection with a default option, ready for you to play around with. + - `RouteChoices`: An Enum built from route names, ensuring you only get valid route selections. + - `compile_runnable`: Sets up the decision-making logic for route selection, guided by the provided instruction and inputs. + - `route_query`: Calls the decision logic with the user's query and a string of route descriptions. + - `selected_route`: The outcome of the decision logic, representing the route to take. + - `assert`: A safety check to confirm the route is a string, as expected by the routes dictionary. + - `handler invocation`: Runs the chosen route's handler with the provided query and additional arguments. + + **Define route functions** + + Now you can use the structured output to execute programatically based on a natural language input. + Establish functions tailored to your needs. + ```python + def handle_pdf_requests(user_query: str) -> str: + return "Handling PDF requests with user query: " + user_query + + + def handle_csv_requests(user_query: str) -> str: + return "Handling CSV requests with user query: " + user_query + + + def handle_default_requests(user_query: str) -> str: + return "Handling DEFAULT requests with user query: " + user_query + ``` + **Define the routes** + And bind the previous established functions. + + ```python + router = DynamicChatRouter( + routes={ + "pdf": { + "handler": handle_pdf_requests, + "description": "Call this for requests including PDF Files.", + }, + "csv": { + "handler": handle_csv_requests, + "description": "Call this for requests including CSV Files.", + }, + "default": { + "handler": handle_default_requests, + "description": "Call this for all other requests.", + }, + }, + ) + ``` + + **Get output** + Use the router.invoke_route method to process the user query and obtain the appropriate response. + + ```python + router.invoke_route("Can you summarize this csv?") + ``` \ No newline at end of file diff --git a/docs/examples/enums.md b/docs/examples/enums.md new file mode 100644 index 0000000..d8b0071 --- /dev/null +++ b/docs/examples/enums.md @@ -0,0 +1,92 @@ +##Decision Making with Enums and Funcchain + +!!! Example + See [enums.py](https://github.com/shroominic/funcchain/blob/main/examples/enums.py) + + In this example, we will use the enum module and funcchain library to build a decision-making system. + This is a useful task for creating applications that require predefined choices or responses. + You can adapt this for your own usage. + This serves as an example of how to implement decision-making logic using enums and the funcchain library. + +##Full Code Example +A simple system that takes a question and decides a 'yes' or 'no' answer based on the input. + +```python +from enum import Enum +from funcchain import chain +from pydantic import BaseModel + +class Answer(str, Enum): + yes = "yes" + no = "no" + +class Decision(BaseModel): + answer: Answer + +def make_decision(question: str) -> Decision: + """ + Based on the question decide yes or no. + """ + return chain() + +if __name__ == "__main__": + print(make_decision("Do you like apples?")) +``` + +#Demo +
+ ```terminal + User: + $ Are apples red? + $ ............... + Decision(answer=) + ``` +
+ +##Instructions + +!!! Step-by-Step + **Necessary Imports** + ```python + from enum import Enum + from funcchain import chain + from pydantic import BaseModel + ``` + + **Define the Answer Enum** + The Answer enum defines possible answers as 'yes' and 'no', which are the only valid responses for the decision-making system. Experiment by using and describing other enums. + + ```python + class Answer(str, Enum): + yes = "yes" + no = "no" + ``` + **Create the Decision Model** + The Decision class uses Pydantic to model a decision, ensuring that the answer is always an instance of the Answer enum. + + ```python + class Decision(BaseModel): + answer: Answer + ``` + + **Implement the Decision Function** + The make_decision function is where the decision logic will be implemented, using `chain()` to process the question and return a decision. + When using your own enums you want to edit this accordingly. + + ```python + def make_decision(question: str) -> Decision: + """ + Based on the question decide yes or no. + """ + return chain() + ``` + + **Run the Decision System** + This block runs the decision-making system, printing out the decision for a given question when the script is executed directly. + + + ```python + if __name__ == "__main__": + print(make_decision("Do you like apples?")) + + ``` \ No newline at end of file diff --git a/docs/examples/error_output.md b/docs/examples/error_output.md new file mode 100644 index 0000000..85629b4 --- /dev/null +++ b/docs/examples/error_output.md @@ -0,0 +1,91 @@ +#Example of raising an error + +!!! Example + error_output.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/error_output.py) + + In this example, we will use the funcchain library to build a system that extracts user information from text. + Most importantly we will be able to raise an error thats programmatically usable. + You can adapt this for your own usage. + + + The main functionality is to take a string of text and attempt to extract user information, such as name and email, and return a User object. If the information is insufficient, an Error is returned instead. + +##Full Code Example + +```python +from funcchain import BaseModel, Error, chain +from rich import print + +class User(BaseModel): + name: str + email: str | None + +def extract_user_info(text: str) -> User | Error: + """ + Extract the user information from the given text. + In case you do not have enough infos, raise. + """ + return chain() + +if __name__ == "__main__": + print(extract_user_info("hey")) # returns Error + print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object + +``` + +Demo +
+ ```python + $ print(extract_user_info("hey")) + + Error: Insufficient information to extract user details. + + User: + $ print(extract_user_info("I'm John and my mail is john@gmail.com")) + + I'm John and my mail is john@gmail.com + User(name='John', email='john@gmail.com') + + //update example + ``` +
+ +##Instructions + +!!! Step-by-Step + + **Necessary Imports** + ```python + from funcchain import BaseModel, Error, chain + from rich import print + ``` + + **Define the User Model** + ```python + class User(BaseModel): + name: str + email: str | None + ``` + The User class is a Pydantic model that defines the structure of the user information to be extracted, with fields for `name` and an email. + Change the fields to experiment and alignment with your project. + + **Implement the Extraction Function** + The `extract_user_info` function is intended to process the input text and return either a User object with extracted information or an Error if the information is not sufficient. + ```python + def extract_user_info(text: str) -> User | Error: + """ + Extract the user information from the given text. + In case you do not have enough infos, raise. + """ + return chain() + ``` + For experiments and adoptions also change the `str` that will be used in chain() to identify what you defined earlier in the `User(BaseModel)` + + + **Run the Extraction System** + This conditional block is used to execute the extraction function and print the results when the script is run directly. + ```python + if __name__ == "__main__": + print(extract_user_info("hey")) # returns Error + print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object + ``` \ No newline at end of file diff --git a/docs/examples/literals.md b/docs/examples/literals.md new file mode 100644 index 0000000..4cf668f --- /dev/null +++ b/docs/examples/literals.md @@ -0,0 +1,88 @@ +#Literal Type Enforcement in Funcchain + +!!! Example + literals.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/literals.py) + + This is a useful task for scenarios where you want to ensure that certain outputs strictly conform to a predefined set of values. + This serves as an example of how to implement strict type checks on outputs using the Literal type from the typing module and the funcchain library. + + You can adapt this for your own usage. + +##Full Code Example + +```python +from typing import Literal +from funcchain import chain +from pydantic import BaseModel + +class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] + +def rank_output(output: str) -> Ranking: + """ + Analyze and rank the output. + """ + return chain() + +if __name__ == "__main__": + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) +``` + +Demo +
+```python +$ rank = rank_output("The quick brown fox jumps over the lazy dog.") +$ ........ +Ranking(chain_of_thought='...', score=33, error='all_good') +``` +
+ +##Instructions + +!!! Step-by-Step + + **Necessary Imports** + ```python + from typing import Literal + from funcchain import chain + from pydantic import BaseModel + ``` + + + **Define the Ranking Model** + The Ranking class is a Pydantic model that uses the Literal type to ensure that the score and error fields can only contain certain predefined values. + So experiment with changing those but keeping this structure of the class. + The LLM will be forced to deliver one of the defined output. + + ```python + class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] + ``` + + **Implement the Ranking Function** + Use `chain()` to process a user input, which must be a string. + Adjust the content based on your above defined class. + + ```python + def rank_output(output: str) -> Ranking: + """ + Analyze and rank the output. + """ + return chain() + ``` + + **Execute the Ranking System** + This block is used to execute the ranking function and print the results when the script is run directly. + ```python + if __name__ == "__main__": + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) + ``` + + + diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index e69de29..1a28715 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -0,0 +1,11 @@ +!!! tip "LETS GOO" + + +
+ +```console + +// You are all set to stream the code content for this tutorial! +``` + +
\ No newline at end of file diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md new file mode 100644 index 0000000..8c40f55 --- /dev/null +++ b/docs/getting-started/introduction.md @@ -0,0 +1,480 @@ +[![Version](https://badge.fury.io/py/funcchain.svg)](https://badge.fury.io/py/funcchain) +[![tests](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml) +![PyVersion](https://img.shields.io/pypi/pyversions/funcchain) +![Downloads](https://img.shields.io/pypi/dm/funcchain) +[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://docs.pydantic.dev/latest/contributing/#badges) +[![Twitter Follow](https://img.shields.io/twitter/follow/shroominic?style=social)](https://x.com/shroominic) + +
+ ```bash + $ > pip install funcchain + ``` +
+ +!!! Important + Dont forget to setup your API if needed for your LLM of choice + + ```bash + export OPENAI_API_KEY="sk-XXX" + ``` + + Or funcchain will automatically detect a .env file. + + Also Useful: Langsmith integration + ```bash + LANGCHAIN_TRACING_V2=true + LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" + LANGCHAIN_API_KEY="ls__XXX" + LANGCHAIN_PROJECT="YOUR_PROJECT" + ``` + Add those lines to .env; funcchain will use Langsmith trace. + + Langsmith is used to understand what happens under the hood of your AI project. + When multiple LLM calls are used for an output they can be logged for debugging. + +## Introduction + +`funcchain` is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. +It works perfect with OpenAI Functions and soon with other models using JSONFormer. + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/ricklamers/funcchain-demo) + +## Simple Demo + +```python +from funcchain import chain +from pydantic import BaseModel + +# define your output shape +class Recipe(BaseModel): + ingredients: list[str] + instructions: list[str] + duration: int + +# write prompts utilising all native python features +def generate_recipe(topic: str) -> Recipe: + """ + Generate a recipe for a given topic. + """ + return chain() # <- this is doing all the magic + +# generate llm response +recipe = generate_recipe("christmas dinner") + +# recipe is automatically converted as pydantic model +print(recipe.ingredients) +``` +!!! Step-by-Step + ```python + # define your output shape + class Recipe(BaseModel): + ingredients: list[str] + instructions: list[str] + duration: int + ``` + + A Recipe class is defined, inheriting from BaseModel (pydantic library). This class + specifies the structure of the output data, which you can customize. + In the example it includes a list of ingredients, a list of instructions, and an integer + representing the duration + + ```python + # write prompts utilising all native python features + def generate_recipe(topic: str) -> Recipe: + """ + Generate a recipe for a given topic. + """ + return chain() # <- this is doing all the magic + ``` + In this example the `generate_recipe` function takes a topic string and returns a `Recipe` instance for that topic. + # Understanding chain() Functionality + Chain() is the backend magic of funcchain. Behind the szenes it creates the prompt executable from the function signature. + Meaning it will turn your function into usable LLM input. + + The `chain()` function does the interaction with the language model to generate a recipe. It accepts several parameters: `system` to specify the model, `instruction` for model directives, `context` to provide relevant background information, `memory` to maintain conversational state, `settings_override` for custom settings, and `**input_kwargs` for additional inputs. Within `generate_recipe`, `chain()` is called with arguments derived from the function's parameters, the function's docstring, or the library's default settings. It compiles these into a Runnable, which then prompts the language model to produce the output. This output is automatically structured into a `Recipe` instance, conforming to the Pydantic model's schema. + + # Get your response + ```python + # generate llm response + recipe = generate_recipe("christmas dinner") + + # recipe is automatically converted as pydantic model + print(recipe.ingredients) + ``` + +#Demo +
+ ``` + $ print(generate_recipe("christmas dinner").ingredients + + ['turkey', 'potatoes', 'carrots', 'brussels sprouts', 'cranberry sauce', 'gravy', + 'butter', 'salt', 'pepper', 'rosemary'] + + ``` +
+ +## Complex Structured Output + +```python +from pydantic import BaseModel, Field +from funcchain import chain + +# define nested models +class Item(BaseModel): + name: str = Field(description="Name of the item") + description: str = Field(description="Description of the item") + keywords: list[str] = Field(description="Keywords for the item") + +class ShoppingList(BaseModel): + items: list[Item] + store: str = Field(description="The store to buy the items from") + +class TodoList(BaseModel): + todos: list[Item] + urgency: int = Field(description="The urgency of all tasks (1-10)") + +# support for union types +def extract_list(user_input: str) -> TodoList | ShoppingList: + """ + The user input is either a shopping List or a todo list. + """ + return chain() + +# the model will choose the output type automatically +lst = extract_list( + input("Enter your list: ") +) + +# custom handler based on type +match lst: + case ShoppingList(items=items, store=store): + print("Here is your Shopping List: ") + for item in items: + print(f"{item.name}: {item.description}") + print(f"You need to go to: {store}") + + case TodoList(todos=todos, urgency=urgency): + print("Here is your Todo List: ") + for item in todos: + print(f"{item.name}: {item.description}") + print(f"Urgency: {urgency}") +``` + +!!! Step-by-Step + **Nececary Imports** + ```python + from pydantic import BaseModel, Field + from funcchain import chain + ``` + + **Data Structures and Model Definitions** + ```python + # define nested models + class Item(BaseModel): + name: str = Field(description="Name of the item") + description: str = Field(description="Description of the item") + keywords: list[str] = Field(description="Keywords for the item") + + class ShoppingList(BaseModel): + items: list[Item] + store: str = Field(description="The store to buy the items from") + + class TodoList(BaseModel): + todos: list[Item] + urgency: int = Field(description="The urgency of all tasks (1-10)") + + ``` + + In this example, Funcchain utilizes Pydantic models to create structured data schemas that facilitate the processing of programmatic inputs. + + You can define new Pydantic models or extend existing ones by adding additional fields or methods. The general approach is to identify the data attributes relevant to your application and create corresponding model classes with these attributes. + + + **Union types** + ```python + # support for union types + def extract_list(user_input: str) -> TodoList | ShoppingList: + """ + The user input is either a shopping List or a todo list. + """ + return chain() + ``` + The extract_list function uses the chain function to analyze user input and return a structured list: + In the example: + - Union Types: It can return either a TodoList or a ShoppingList, depending on the input. + - Usage of chain: chain simplifies the process, deciding the type of list to return. + + For your application this is going to serve as a router to route between your previously defined models. + + **Get a list from the user** (here as "lst") + ```python + # the model will choose the output type automatically + lst = extract_list( + input("Enter your list: ") + ) + + ``` + + **Define your custom handlers** + + And now its time to define what happens with the result. + You can then use the lst variable to match. + + ```python + # custom handler based on type + match lst: + case ShoppingList(items=items, store=store): + print("Here is your Shopping List: ") + for item in items: + print(f"{item.name}: {item.description}") + print(f"You need to go to: {store}") + + case TodoList(todos=todos, urgency=urgency): + print("Here is your Todo List: ") + for item in todos: + print(f"{item.name}: {item.description}") + print(f"Urgency: {urgency}") + + ``` + +#Demo +
+ ``` + lst = extract_list( + input("Enter your list: ") + ) + + User: + $ Complete project report, Prepare for meeting, Respond to emails; + $ if I don't respond I will be fired + + Output: + $ ............... + Here is your Todo List: + Complete your buisness tasks: project report, Prepare for meeting, Respond to emails + Urgency: 10 + //add real output + ``` +
+ + + + + + + +## Vision Models + +```python +from PIL import Image +from pydantic import BaseModel, Field +from funcchain import chain, settings + +# set global llm using model identifiers (see MODELS.md) +settings.llm = "openai/gpt-4-vision-preview" + +# everything defined is part of the prompt +class AnalysisResult(BaseModel): + """The result of an image analysis.""" + + theme: str = Field(description="The theme of the image") + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + +# easy use of images as input with structured output +def analyse_image(image: Image.Image) -> AnalysisResult: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + +result = analyse_image(Image.open("examples/assets/old_chinese_temple.jpg")) + +print("Theme:", result.theme) +print("Description:", result.description) +for obj in result.objects: + print("Found this object:", obj) +``` +!!! Step-by-Step + **Nececary Imports** + ```python + from PIL import Image + from pydantic import BaseModel, Field + from funcchain import chain, settings + ``` + + **Define Model** + set global llm using model identifiers see [MODELS.md]((https://github.com/shroominic/funcchain/blob/main/MODELS.md)) + ```python + settings.llm = "openai/gpt-4-vision-preview" + ``` + Funcchains modularity allows for all kinds of models including local models + + + **Analize Image** + Get structured output from an image in our example `theme`, `description` and `objects` + ```python + # everything defined is part of the prompt + class AnalysisResult(BaseModel): + """The result of an image analysis.""" + + theme: str = Field(description="The theme of the image") + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + ``` + Adjsut the fields as needed. Play around with the example, feel free to experiment. + You can customize the analysis by modifying the fields of the `AnalysisResult` model. + + **Function to start the analysis** + + ```python + # easy use of images as input with structured output + def analyse_image(image: Image.Image) -> AnalysisResult: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + ``` + Chain() will handle the image input. + We here define again the fields from before `theme`, `description` and `objects` + + give an image as input `image: Image.Image` + + Its important that the fields defined earlier are mentioned here with the prompt + `Analyse the image and extract its`... + +#Demo +
+ ``` + print(analyse_image(image: Image.Image)) + + $ .................. + + Theme: Nature + Description: A beautiful landscape with a mountain range in the background, a clear blue sky, and a calm lake in the foreground surrounded by greenery. + Found this object: mountains + Found this object: sky + Found this object: lake + Found this object: trees + Found this object: grass + + ``` +
+ + +## Seamless local model support +Yes you can use funcchain without internet connection. +Start heating up your device. + +```python +from pydantic import BaseModel, Field +from funcchain import chain, settings + +# auto-download the model from huggingface +settings.llm = "ollama/openchat" + +class SentimentAnalysis(BaseModel): + analysis: str + sentiment: bool = Field(description="True for Happy, False for Sad") + +def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + +# generates using the local model +poem = analyze("I really like when my dog does a trick!") + +# promised structured output (for local models!) +print(poem.analysis) +``` +!!! Step-by-Step + **Nececary Imports** + ```python + from pydantic import BaseModel, Field + from funcchain import chain, settings + ``` + + **Choose and enjoy** + ```python + # auto-download the model from huggingface + settings.llm = "ollama/openchat" + ``` + + **Structured output definition** + With an input `str` a description can be added to return a boolean `true` or `false` + ```python + class SentimentAnalysis(BaseModel): + analysis: str + sentiment: bool = Field(description="True for Happy, False for Sad") + ``` + Experiment yourself by adding different descriptions for the true and false case. + + **Use `chain()` to analize** + Defines with natural language the analysis + ```python + def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + ``` + For your own usage adjust the str. Be precise and reference your classes again. + + **Generate and print the output** + ```python + **Use the analyze function and print output** + + # generates using the local model + poem = analyze("I really like when my dog does a trick!") + + # promised structured output (for local models!) + print(poem.analysis) + ``` +#Demo +
+ ``` + poem = analyze("I really like when my dog does a trick!") + + $ .................. + + Add demo + + ``` +
+ + +## Features + +- **🎨 Minimalistic and Easy to Use**: Designed with simplicity in mind for straightforward usage. +- **🔄 Model Flexibility**: Effortlessly switch between OpenAI and local models. +- **📝 Pythonic Prompts**: Craft natural language prompts as intuitive Python functions. +- **🔧 Structured Output**: Define output schemas with Pydantic models. +- **🚀 Powered by LangChain**: Utilize the robust LangChain core for backend operations. +- **🧩 Template Support**: Employ f-strings or Jinja templates for dynamic prompt creation. +- **🔗 Integration with AI Services**: Take full advantage of OpenAI Functions or LlamaCpp Grammars. +- **🛠️ Langsmith Support**: Ensure compatibility with Langsmith for superior language model interactions. +- **⚡ Asynchronous and Pythonic**: Embrace modern Python async features. +- **🤗 Huggingface Integration**: Automatically download models from Huggingface. +- **🌊 Streaming Support**: Enable real-time streaming for interactive applications. + +## Documentation + +Highly recommend to try out the examples in the `./examples` folder. + +Coming soon... feel free to add helpful .md files :) + +## Contribution + +You want to contribute? That's great! Please run the dev setup to get started: + +```bash +> git clone https://github.com/shroominic/funcchain.git && cd funcchain + +> ./dev_setup.sh +``` + +Thanks! diff --git a/docs/index.md b/docs/index.md index dd00a0a..8af55be 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,8 +8,11 @@ ## Welcome -funcchain is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. -It works perfect with OpenAI Functions and soon with other models using JSONFormer. + +!!! Description + funcchain is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. + It works perfect with OpenAI Functions and soon with other models using JSONFormer. + Key features: @@ -23,28 +26,31 @@ Key features: ## Installation -```bash -pip install funcchain -``` +
+ ```bash + # pip install funcchain + ``` +
-Make sure to have an OpenAI API key in your environment variables. For example, +!!! Important + Make sure to have an OpenAI API key in your environment variables. For example, -```bash -export OPENAI_API_KEY=sk-********** -``` + ```bash + export OPENAI_API_KEY=sk-********** + ``` ## Usage -```python -from funcchain import chain - -def hello() -> str: - """Say hello in 3 languages""" - return chain() + ```python + from funcchain import chain -print(hello()) # -> Hello, Bonjour, Hola -``` + def hello() -> str: + """Say hello in 3 languages""" + return chain() + print(hello()) # -> Hello, Bonjour, Hola + ``` ++ This will call the OpenAI API and return the response. The `chain` function extracts the docstring as the prompt and the return type for parsing the response. @@ -53,10 +59,11 @@ The `chain` function extracts the docstring as the prompt and the return type fo To contribute, clone the repo and run: +
```bash -./dev_setup.sh +# ./dev_setup.sh ``` - +
This will install pre-commit hooks, dependencies and set up the environment. To activate the virtual environment managed by poetry, you can use the following command: diff --git a/docs/js/custom.js b/docs/js/custom.js new file mode 100644 index 0000000..79d0a1f --- /dev/null +++ b/docs/js/custom.js @@ -0,0 +1,113 @@ +function setupTermynal() { + document.querySelectorAll(".use-termynal").forEach(node => { + node.style.display = "block"; + new Termynal(node, { + lineDelay: 500 + }); + }); + const progressLiteralStart = "---> 100%"; + const promptLiteralStart = "$ "; + const customPromptLiteralStart = "# "; + const termynalActivateClass = "termy"; + let termynals = []; + + function createTermynals() { + document + .querySelectorAll(`.${termynalActivateClass} .highlight`) + .forEach(node => { + const text = node.textContent; + const lines = text.split("\n"); + const useLines = []; + let buffer = []; + function saveBuffer() { + if (buffer.length) { + let isBlankSpace = true; + buffer.forEach(line => { + if (line) { + isBlankSpace = false; + } + }); + dataValue = {}; + if (isBlankSpace) { + dataValue["delay"] = 0; + } + if (buffer[buffer.length - 1] === "") { + // A last single
won't have effect + // so put an additional one + buffer.push(""); + } + const bufferValue = buffer.join("
"); + dataValue["value"] = bufferValue; + useLines.push(dataValue); + buffer = []; + } + } + for (let line of lines) { + if (line === progressLiteralStart) { + saveBuffer(); + useLines.push({ + type: "progress" + }); + } else if (line.startsWith(promptLiteralStart)) { + saveBuffer(); + const value = line.replace(promptLiteralStart, "").trimEnd(); + useLines.push({ + type: "input", + value: value + }); + } else if (line.startsWith("// ")) { + saveBuffer(); + const value = "💬 " + line.replace("// ", "").trimEnd(); + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0 + }); + } else if (line.startsWith(customPromptLiteralStart)) { + saveBuffer(); + const promptStart = line.indexOf(promptLiteralStart); + if (promptStart === -1) { + console.error("Custom prompt found but no end delimiter", line) + } + const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") + let value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt + }); + } else { + buffer.push(line); + } + } + saveBuffer(); + const div = document.createElement("div"); + node.replaceWith(div); + const termynal = new Termynal(div, { + lineData: useLines, + noInit: true, + lineDelay: 500 + }); + termynals.push(termynal); + }); + } + + function loadVisibleTermynals() { + termynals = termynals.filter(termynal => { + if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { + termynal.init(); + return false; + } + return true; + }); + } + window.addEventListener("scroll", loadVisibleTermynals); + createTermynals(); + loadVisibleTermynals(); +} + +async function main() { + setupTermynal() +} + +main() \ No newline at end of file diff --git a/docs/js/termynal.js b/docs/js/termynal.js new file mode 100644 index 0000000..7146d8d --- /dev/null +++ b/docs/js/termynal.js @@ -0,0 +1,263 @@ +/** + * termynal.js + * A lightweight, modern and extensible animated terminal window, using + * async/await. + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + +'use strict'; + +/** Generate a terminal widget. */ +class Termynal { + /** + * Construct the widget's settings. + * @param {(string|Node)=} container - Query selector or container element. + * @param {Object=} options - Custom settings. + * @param {string} options.prefix - Prefix to use for data attributes. + * @param {number} options.startDelay - Delay before animation, in ms. + * @param {number} options.typeDelay - Delay between each typed character, in ms. + * @param {number} options.lineDelay - Delay between each line, in ms. + * @param {number} options.progressLength - Number of characters displayed as progress bar. + * @param {string} options.progressChar – Character to use for progress bar, defaults to █. + * @param {number} options.progressPercent - Max percent of progress. + * @param {string} options.cursor – Character to use for cursor, defaults to ▋. + * @param {Object[]} lineData - Dynamically loaded line data objects. + * @param {boolean} options.noInit - Don't initialise the animation. + */ + constructor(container = '#termynal', options = {}) { + this.container = (typeof container === 'string') ? document.querySelector(container) : container; + this.pfx = `data-${options.prefix || 'ty'}`; + this.originalStartDelay = this.startDelay = options.startDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; + this.originalTypeDelay = this.typeDelay = options.typeDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; + this.originalLineDelay = this.lineDelay = options.lineDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; + this.progressLength = options.progressLength + || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; + this.progressChar = options.progressChar + || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; + this.progressPercent = options.progressPercent + || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; + this.cursor = options.cursor + || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; + this.lineData = this.lineDataToElements(options.lineData || []); + this.loadLines() + if (!options.noInit) this.init() + } + + loadLines() { + // Load all the lines and create the container so that the size is fixed + // Otherwise it would be changing and the user viewport would be constantly + // moving as she/he scrolls + const finish = this.generateFinish() + finish.style.visibility = 'hidden' + this.container.appendChild(finish) + // Appends dynamically loaded lines to existing line elements. + this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); + for (let line of this.lines) { + line.style.visibility = 'hidden' + this.container.appendChild(line) + } + const restart = this.generateRestart() + restart.style.visibility = 'hidden' + this.container.appendChild(restart) + this.container.setAttribute('data-termynal', ''); + } + + /** + * Initialise the widget, get lines, clear container and start animation. + */ + init() { + /** + * Calculates width and height of Termynal container. + * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. + */ + const containerStyle = getComputedStyle(this.container); + this.container.style.width = containerStyle.width !== '0px' ? + containerStyle.width : undefined; + this.container.style.minHeight = containerStyle.height !== '0px' ? + containerStyle.height : undefined; + + this.container.setAttribute('data-termynal', ''); + this.container.innerHTML = ''; + for (let line of this.lines) { + line.style.visibility = 'visible' + } + this.start(); + } + + /** + * Start the animation and rener the lines depending on their data attributes. + */ + async start() { + this.addFinish() + await this._wait(this.startDelay); + + for (let line of this.lines) { + const type = line.getAttribute(this.pfx); + const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; + + if (type == 'input') { + line.setAttribute(`${this.pfx}-cursor`, this.cursor); + await this.type(line); + await this._wait(delay); + } + + else if (type == 'progress') { + await this.progress(line); + await this._wait(delay); + } + + else { + this.container.appendChild(line); + await this._wait(delay); + } + + line.removeAttribute(`${this.pfx}-cursor`); + } + this.addRestart() + this.finishElement.style.visibility = 'hidden' + this.lineDelay = this.originalLineDelay + this.typeDelay = this.originalTypeDelay + this.startDelay = this.originalStartDelay + } + + generateRestart() { + const restart = document.createElement('a') + restart.onclick = (e) => { + e.preventDefault() + this.container.innerHTML = '' + this.init() + } + restart.href = '#' + restart.setAttribute('data-terminal-control', '') + restart.innerHTML = "restart ↻" + return restart + } + + generateFinish() { + const finish = document.createElement('a') + finish.onclick = (e) => { + e.preventDefault() + this.lineDelay = 0 + this.typeDelay = 0 + this.startDelay = 0 + } + finish.href = '#' + finish.setAttribute('data-terminal-control', '') + finish.innerHTML = "fast →" + this.finishElement = finish + return finish + } + + addRestart() { + const restart = this.generateRestart() + this.container.appendChild(restart) + } + + addFinish() { + const finish = this.generateFinish() + this.container.appendChild(finish) + } + + /** + * Animate a typed line. + * @param {Node} line - The line element to render. + */ + async type(line) { + const chars = [...line.textContent]; + line.textContent = ''; + this.container.appendChild(line); + + for (let char of chars) { + const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; + await this._wait(delay); + line.textContent += char; + } + } + + /** + * Animate a progress bar. + * @param {Node} line - The line element to render. + */ + async progress(line) { + const progressLength = line.getAttribute(`${this.pfx}-progressLength`) + || this.progressLength; + const progressChar = line.getAttribute(`${this.pfx}-progressChar`) + || this.progressChar; + const chars = progressChar.repeat(progressLength); + const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) + || this.progressPercent; + line.textContent = ''; + this.container.appendChild(line); + + for (let i = 1; i < chars.length + 1; i++) { + await this._wait(this.typeDelay); + const percent = Math.round(i / chars.length * 100); + line.textContent = `${chars.slice(0, i)} ${percent}%`; + if (percent>progressPercent) { + break; + } + } + } + + /** + * Helper function for animation delays, called with `await`. + * @param {number} time - Timeout, in ms. + */ + _wait(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + + /** + * Converts line data objects into line elements. + * + * @param {Object[]} lineData - Dynamically loaded lines. + * @param {Object} line - Line data object. + * @returns {Element[]} - Array of line elements. + */ + lineDataToElements(lineData) { + return lineData.map(line => { + let div = document.createElement('div'); + div.innerHTML = `${line.value || ''}`; + + return div.firstElementChild; + }); + } + + /** + * Helper function for generating attributes string. + * + * @param {Object} line - Line data object. + * @returns {string} - String of attributes. + */ + _attributes(line) { + let attrs = ''; + for (let prop in line) { + // Custom add class + if (prop === 'class') { + attrs += ` class=${line[prop]} ` + continue + } + if (prop === 'type') { + attrs += `${this.pfx}="${line[prop]}" ` + } else if (prop !== 'value') { + attrs += `${this.pfx}-${prop}="${line[prop]}" ` + } + } + return attrs; + } +} + +/** +* HTML API: If current script has container(s) specified, initialise Termynal. +*/ +if (document.currentScript.hasAttribute('data-termynal-container')) { + const containers = document.currentScript.getAttribute('data-termynal-container'); + containers.split('|') + .forEach(container => new Termynal(container)) +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 0e882bd..3cb00dd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,21 +5,88 @@ repo_name: shroominic/funcchain repo_url: https://github.com/shroominic/funcchain/ nav: + - 'Introduction': + - 'Funcchain': 'getting-started/introduction.md' - 'Getting Started': - 'Welcome': 'index.md' - 'Installation': 'getting-started/installation.md' - 'Usage': 'getting-started/usage.md' - 'Concepts': - - 'Overview': 'overview.md' + - 'Overview': 'concepts/overview.md' - 'Chain': 'chain.md' - 'Input Args': 'input.md' - 'Prompt Template': 'prompt.md' - 'Output Parser': 'parser.md' - 'Pydantic Models': 'models.md' - 'Settings': 'settings.md' - - 'Examples': 'examples.md' + - 'Examples': + - 'ChatGPT': 'examples/chat.md' + - 'Dynamic Router': 'examples/dynamic_router.md' + - 'Enums': 'examples/enums.md' + - 'Error Output': 'examples/error_output.md' + - 'Literals': 'examples/literals.md' + - 'Union Types': 'examples/union.md' theme: name: material palette: scheme: slate + +# Extensions +markdown_extensions: + - abbr + - admonition + - pymdownx.details + - attr_list + - def_list + - footnotes + - md_in_html + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_generator: !!python/name:material.extensions.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink: + normalize_issue_symbols: true + repo_url_shorthand: true + user: jxnl + repo: instructor + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.snippets: + auto_append: + - includes/mkdocs.md + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + combine_header_slug: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +extra_css: + - css/termynal.css + - css/custom.css + +extra_javascript: + - js/termynal.js + - js/custom.js \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock index db438bb..c70599f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -30,6 +30,7 @@ faiss-cpu==1.7.4 filelock==3.13.1 frozenlist==1.4.0 ghp-import==2.1.0 +greenlet==3.0.3 h11==0.14.0 httpcore==1.0.2 httpx==0.25.1 @@ -67,13 +68,11 @@ packaging==23.2 paginate==0.5.6 parso==0.8.3 pathspec==0.11.2 -pexpect==4.9.0 pillow==10.1.0 platformdirs==4.0.0 pluggy==1.3.0 pre-commit==3.5.0 prompt-toolkit==3.0.41 -ptyprocess==0.7.0 pure-eval==0.2.2 pydantic==2.5.2 pydantic-core==2.14.5 diff --git a/requirements.lock b/requirements.lock index 14c4468..1820e79 100644 --- a/requirements.lock +++ b/requirements.lock @@ -11,6 +11,7 @@ annotated-types==0.6.0 anyio==3.7.1 certifi==2023.11.17 charset-normalizer==3.3.2 +colorama==0.4.6 distro==1.9.0 docstring-parser==0.15 h11==0.14.0 From 088bc4d441ed1079293df9e040236843bef42883 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:32 +0000 Subject: [PATCH 232/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20ChatModel=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MODELS.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/MODELS.md b/MODELS.md index 0e1a951..5958f32 100644 --- a/MODELS.md +++ b/MODELS.md @@ -2,7 +2,7 @@ ## LangChain Chat Models -You can set the `settings.llm` with any ChatModel the LangChain library. +You can set the `settings.llm` with any LangChain ChatModel. ```python from langchain_openai.chat_models import AzureChatOpenAI @@ -16,22 +16,28 @@ You can also set the `settings.llm` with a string identifier of a ChatModel incl ### Schema -`/:` +`/:` ### Providers - `openai`: OpenAI Chat Models -- `ollama`: Run local models through Ollama(llamacpp) +- `llamacpp`: Run local models directly using llamacpp (alias: `thebloke`, `gguf`) +- `ollama`: Run local models through Ollama (wrapper for llamacpp) +- `azure`: Azure Chat Models +- `anthropic`: Anthropic Chat Models +- `google`: Google Chat Models ### Examples -- `openai/gpt-3.5-turbo`: Classic ChatGPT +- `openai/gpt-3.5-turbo`: ChatGPT Classic - `openai/gpt-4-1106-preview`: GPT-4-Turbo -- `ollama/openchat-3.5- - `ollama/openchat`: OpenChat3.5-1210 - `ollama/openhermes2.5-mistral`: OpenHermes 2.5 +- `llamacpp/openchat-3.5-1210`: OpenChat3.5-1210 +- `TheBloke/Nous-Hermes-2-SOLAR-10.7B-GGUF`: alias for `llamacpp/...` +- `TheBloke/openchat-3.5-0106-GGUF:Q3_K_L`: with Q label ### additional notes -Checkout the file `src/funcchain/utils/model_defaults.py` for the code that parses the string identifier. +Checkout the file `src/funcchain/model/defaults.py` for the code that parses the string identifier. Feel free to create a PR to add more models to the defaults. Or tell me how wrong I am and create a better system. From 1d0228adc54d83d88d8aab8b03192f458700abe8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:32 +0000 Subject: [PATCH 233/451] =?UTF-8?q?=F0=9F=94=A5=20Simplify=20print=20state?= =?UTF-8?q?ment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/decorator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/decorator.py b/examples/decorator.py index 7f29c7f..aded07f 100644 --- a/examples/decorator.py +++ b/examples/decorator.py @@ -28,6 +28,4 @@ def generate_poem(topic: str, context: str) -> str: } | generate_poem -result = retrieval_chain.invoke("love") - -print(result) +print(retrieval_chain.invoke("love")) From 6900b3047dabbe43f389a1e93384065b3954ebb9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:32 +0000 Subject: [PATCH 234/451] =?UTF-8?q?=F0=9F=94=A7=20Adjust=20output=5Ftypes?= =?UTF-8?q?=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/dynamic_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dynamic_router.py b/examples/dynamic_router.py index 7fb371b..cbff138 100644 --- a/examples/dynamic_router.py +++ b/examples/dynamic_router.py @@ -34,7 +34,7 @@ class RouterModel(BaseModel): route_query = compile_runnable( instruction="Given the user query select the best query handler for it.", input_args=["user_query", "query_handlers"], - output_types=(RouterModel,), + output_types=[RouterModel], ) selected_route = route_query.invoke( From 68f11d4b4fcf6c7c05fbd6512917a80f39585e5b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 235/451] =?UTF-8?q?=E2=9C=A8=20Add=20Jinja=20example=20scr?= =?UTF-8?q?ipt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/jinja.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 examples/jinja.py diff --git a/examples/jinja.py b/examples/jinja.py new file mode 100644 index 0000000..a82fbc7 --- /dev/null +++ b/examples/jinja.py @@ -0,0 +1,30 @@ +from funcchain import chain, settings +from pydantic import BaseModel + +settings.console_stream = True + + +class Cart(BaseModel): + items: list[str] + price: float + + +def shopping_analysis(cart: Cart, f_instructions: bool) -> str: + """ + Shopping List: + {% for item in cart.items %} - {{ item }} + {% endfor %} + + Determine if the cart is healthy or not and if the price is good. + {% if f_instructions %} format the output as json! {% endif %} + """ + return chain() + + +example_cart = Cart( + items=["apple", "banana", "orange", "mango", "pineapple"], + price=2.99, +) + +print(shopping_analysis(example_cart, True)) +print(shopping_analysis(example_cart, False)) From 0445ee3150c99d0dbd18c21493751fcaf2d4fbf9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 236/451] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20attrib?= =?UTF-8?q?ute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/literals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/literals.py b/examples/literals.py index b633c15..5948e5e 100644 --- a/examples/literals.py +++ b/examples/literals.py @@ -6,7 +6,6 @@ # just a silly example to schowcase the Literal type class Ranking(BaseModel): - chain_of_thought: str score: Literal[11, 22, 33, 44, 55] error: Literal["no_input", "all_good", "invalid"] From cb8ac1004bfcd6c7a655a1eaa93f5ef8614b7f4b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 237/451] =?UTF-8?q?=E2=9C=A8=20Add=20sentiment=20analysis?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/llamacpp.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 examples/llamacpp.py diff --git a/examples/llamacpp.py b/examples/llamacpp.py new file mode 100644 index 0000000..29c90c5 --- /dev/null +++ b/examples/llamacpp.py @@ -0,0 +1,30 @@ +from funcchain import chain, settings +from pydantic import BaseModel, Field +from rich import print + + +# define your model +class SentimentAnalysis(BaseModel): + analysis: str = Field(description="A description of the analysis") + sentiment: bool = Field(description="True for Happy, False for Sad") + + +# define your prompt +def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + + +if __name__ == "__main__": + # set global llm + settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" + # log tokens as stream to console + settings.console_stream = True + + # run prompt + poem = analyze("I really like when my dog does a trick!") + + # show final parsed output + print(poem) From 19bcd89988b320c1e69f6e036aa6280164e5f3ee Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 238/451] =?UTF-8?q?=E2=9C=A8=20Add=20primitive=20types=20e?= =?UTF-8?q?xample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/primitive_types.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/primitive_types.py diff --git a/examples/primitive_types.py b/examples/primitive_types.py new file mode 100644 index 0000000..1296f9e --- /dev/null +++ b/examples/primitive_types.py @@ -0,0 +1,17 @@ +from typing import Literal + +from funcchain import chain, settings + +settings.console_stream = True + + +def evaluate(sentence: str) -> tuple[Literal["good", "bad"], float, str]: + """ + Evaluate the given sentence based on grammatical correctness and give it a score. + """ + return chain() + + +result = evaluate("Hello, I am new to english language. Let's see how well I can write.") + +print(type(result)) From 8443df5181ed7a01f96fbd0430cac348227ae005 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 239/451] =?UTF-8?q?=F0=9F=94=A7=20Remove=20unused=20depend?= =?UTF-8?q?encies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef8b3b0..a30c419 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ authors = [ { name = "Shroominic", email = "contact@shroominic.com" } ] dependencies = [ - "langchain_core>=0.1", "langchain_openai>=0.0.3", "pydantic-settings>=2", "docstring-parser>=0.15", @@ -36,19 +35,15 @@ build-backend = "hatchling.build" [tool.rye] managed = true dev-dependencies = [ + "funcchain[all]", "ruff", "mypy", "isort", "pytest", "ipython", "pre-commit", - "langchain>=0.1", - "funcchain[all]", "types-PyYAML>=6", "mkdocs-material>=9.4", - "beautifulsoup4>=4.12", - "python-dotenv>=1", - "faiss-cpu>=1.7.4", ] [project.optional-dependencies] @@ -58,13 +53,23 @@ openai = [ ollama = [ "langchain_community", ] +llamacpp = [ + "llama-cpp-python>=0.2.32", + "huggingface_hub>=0.20", +] pillow = [ "pillow", ] +example-extras = [ + "langchain>=0.1", + "faiss-cpu>=1.7.4", + "beautifulsoup4>=4.12", +] all = [ + "funcchain[pillow]", "funcchain[openai]", "funcchain[ollama]", - "funcchain[pillow]", + "funcchain[llamacpp]", "langchain", ] From a6f263ec9a5c60dfa4c6fb9aa37d6ee2bc1957a5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 240/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20development=20d?= =?UTF-8?q?ependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index e6702b0..c1f79c9 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -14,7 +14,6 @@ anyio==4.2.0 asttokens==2.4.1 attrs==23.2.0 babel==2.14.0 -beautifulsoup4==4.12.3 certifi==2023.11.17 cfgv==3.4.0 charset-normalizer==3.3.2 @@ -22,17 +21,19 @@ click==8.1.7 colorama==0.4.6 dataclasses-json==0.6.3 decorator==5.1.1 +diskcache==5.6.3 distlib==0.3.8 distro==1.9.0 docstring-parser==0.15 executing==2.0.1 -faiss-cpu==1.7.4 filelock==3.13.1 frozenlist==1.4.1 +fsspec==2023.12.2 ghp-import==2.1.0 h11==0.14.0 httpcore==1.0.2 httpx==0.26.0 +huggingface-hub==0.20.3 identify==2.5.33 idna==3.6 iniconfig==2.0.0 @@ -47,6 +48,7 @@ langchain-community==0.0.15 langchain-core==0.1.15 langchain-openai==0.0.3 langsmith==0.0.83 +llama-cpp-python==0.2.32 markdown==3.5.2 markdown-it-py==3.0.0 markupsafe==2.1.4 @@ -91,7 +93,6 @@ rich==13.7.0 ruff==0.1.14 six==1.16.0 sniffio==1.3.0 -soupsieve==2.5 sqlalchemy==2.0.25 stack-data==0.6.3 tenacity==8.2.3 From 364b419a95cec2de66a8dc857cffb6298729354c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 241/451] =?UTF-8?q?=F0=9F=94=84=20Update=20roadmap=20prior?= =?UTF-8?q?ities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmap.todo | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/roadmap.todo b/roadmap.todo index f0b021b..cdb4dd8 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -1,3 +1,13 @@ +IMPORTANT: +[ ] - enable union type without function calling (6h) +[ ] - improve docs (8h) + +COMING SOON: + +[ ] - pydantic model streaming (6h) + +[ ] - cookbooks folder with jupyter notebook tutorials (8h) + [ ] - depends functionality to create nested chains and compile into runnables (10h) # add a deps thing to put into funcchain defs that takes another chain and compiles it into a runnable # so langsmith shows nested chains @@ -11,32 +21,14 @@ # So anything that is additional can be compressed to fit in the context but when other things that are important are not compressed. # Optionally you can define how to compress and where to leave the gaps (default in the middle with [...]) -[ ] - enable union type without function calling (8h) - [ ] - enable Error type for non union calls (4h) -[ ] - funcchain Agent Framework with Task Dependencies (30h) +[ ] - LLMCompiler written in funcchain example (30h) [ ] - convert langchain tools to funcchain agent/router (8h) [ ] - vscode extension for custom syntax highlighting (30h) -[ ] - migrate to jinja2 (6h) - -[ ] - allow images as urls (2h) - -[ ] - brainstorm easy async helpers (4h) - -[ ] - cookbooks folder with jupyter notebook tutorials (6h) - [ ] - parallel function calling (8h) [ ] - FuncUnion and str output (6h) - -[ ] - implement vision over llamacpp (8h) - -[ ] - document examples (6h) - -[ ] - split required/optional deps for only local or only openai ... - -[ ] - opt in token counting callback handler as setting to log tokens From 80999cfd997ea0b5d39ed9a616d64cfb476a7b00 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 242/451] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20compiler=20ty?= =?UTF-8?q?pes,=20parsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/compiler.py | 70 ++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/src/funcchain/backend/compiler.py b/src/funcchain/backend/compiler.py index ade8e7c..f900a9f 100644 --- a/src/funcchain/backend/compiler.py +++ b/src/funcchain/backend/compiler.py @@ -10,7 +10,9 @@ from ..model.abilities import is_openai_function_model, is_vision_model from ..model.defaults import univeral_model_selector +from ..parser.json_schema import RetryJsonPydanticParser from ..parser.openai_functions import RetryOpenAIFunctionPydanticParser, RetryOpenAIFunctionPydanticUnionParser +from ..parser.primitive_types import RetryJsonPrimitiveTypeParser from ..parser.schema_converter import pydantic_to_grammar from ..parser.selector import parser_for from ..schema.signature import Signature @@ -32,7 +34,7 @@ # TODO: do patch instead of seperate creation def create_union_chain( - output_types: tuple[type], + output_types: list[type], instruction_prompt: HumanImageMessagePromptTemplate, system: str, memory: BaseChatMessageHistory, @@ -86,7 +88,7 @@ def patch_openai_function_to_pydantic( def create_chain( system: str, instruction: str, - output_types: tuple[type[ChainOutput]], + output_types: list[type[ChainOutput]], context: list[BaseMessage], memory: BaseChatMessageHistory, settings: FuncchainSettings, @@ -102,6 +104,14 @@ def create_chain( parser = parser_for(output_types, retry=settings.retry_parse, llm=llm) + # TODO collect types from input_args + # -> this would allow special prompt templating based on certain types + # -> e.g. BaseChatMessageHistory adds a history placeholder + # -> e.g. BaseChatModel overrides the default language model + # -> e.g. SettingsOverride overrides the default settings + # -> e.g. Callbacks adds custom callbacks + # -> e.g. SystemMessage adds a system message + # handle input arguments prompt_args: list[str] = [] pydantic_args: list[str] = [] @@ -156,7 +166,7 @@ def create_chain( # # add formatted instruction to chat history # memory.add_message(instruction_prompt.format(**input_kwargs)) - _inject_grammar_for_local_models(llm, output_types) + _inject_grammar_for_local_models(llm, output_types, parser) # function model patches if is_openai_function_model(llm): @@ -170,20 +180,20 @@ def create_chain( llm, input_kwargs, ) - output_type = output_types[0] - if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): - if settings.streaming and hasattr(llm, "model_kwargs"): - llm.model_kwargs = {"response_format": {"type": "json_object"}} - else: - llm, parser = patch_openai_function_to_pydantic(llm, output_type, input_kwargs) + if isinstance(parser, RetryJsonPydanticParser) or isinstance(parser, RetryJsonPrimitiveTypeParser): + output_type = parser.pydantic_object + if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): + if settings.streaming and hasattr(llm, "model_kwargs"): + llm.model_kwargs = {"response_format": {"type": "json_object"}} + else: + assert isinstance(parser, RetryJsonPydanticParser) + llm, parser = patch_openai_function_to_pydantic(llm, output_type, input_kwargs) assert parser is not None return chat_prompt | llm | parser -def compile_chain( - signature: Signature[ChainOutput], temp_images: list[Image] = [] -) -> Runnable[dict[str, str], ChainOutput]: +def compile_chain(signature: Signature, temp_images: list[Image] = []) -> Runnable[dict[str, str], ChainOutput]: """ Compile a signature to a runnable chain. """ @@ -196,7 +206,7 @@ def compile_chain( memory = ChatMessageHistory(messages=signature.history) return create_chain( - msg_to_str(system), + msg_to_str(system) if system else "", signature.instruction, signature.output_types, signature.history, @@ -268,13 +278,14 @@ def _handle_images( def _inject_grammar_for_local_models( llm: BaseChatModel, - output_types: tuple[type], + output_types: list[type], + parser: BaseOutputParser | BaseGenerationOutputParser, ) -> None: """ Inject GBNF grammar into local models. """ try: - from funcchain.model.llm_overrides import ChatOllama + from funcchain.model.patches.ollama import ChatOllama except: # noqa pass else: @@ -283,9 +294,38 @@ def _inject_grammar_for_local_models( raise NotImplementedError("Union types are not yet supported for LlamaCpp models.") # TODO: implement output_type = output_types[0] if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): + assert isinstance(parser, RetryJsonPydanticParser) + output_type = parser.pydantic_object llm.grammar = pydantic_to_grammar(output_type) if issubclass(output_type, ParserBaseModel): llm.grammar = output_type.custom_grammar() + try: + from llama_cpp import LlamaGrammar + + from ..model.patches.llamacpp import ChatLlamaCpp + except: # noqa + pass + else: + if isinstance(llm, ChatLlamaCpp): + if len(output_types) > 1: # TODO: implement + raise NotImplementedError("Union types are not yet supported for LlamaCpp models.") + + output_type = output_types[0] + if isinstance(parser, RetryJsonPydanticParser) or isinstance(parser, RetryJsonPrimitiveTypeParser): + output_type = parser.pydantic_object + + if issubclass(output_type, BaseModel) and not issubclass(output_type, ParserBaseModel): + assert isinstance(parser, RetryJsonPydanticParser) + output_type = parser.pydantic_object + grammar: str | None = pydantic_to_grammar(output_type) + if issubclass(output_type, ParserBaseModel): + grammar = output_type.custom_grammar() + if grammar: + setattr( + llm, + "grammar", + LlamaGrammar.from_string(grammar, verbose=False), + ) def _gather_llm(settings: FuncchainSettings) -> BaseChatModel: From 7a5651369e121bb87e466a0448695b7790fdb53a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 243/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20type=20annotati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/meta_inspect.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/funcchain/backend/meta_inspect.py b/src/funcchain/backend/meta_inspect.py index 0266dd1..15f265f 100644 --- a/src/funcchain/backend/meta_inspect.py +++ b/src/funcchain/backend/meta_inspect.py @@ -38,17 +38,17 @@ def from_docstring(f: Optional[FunctionType] = None) -> str: raise ValueError(f"The funcchain ({get_parent_frame().function}) must have a docstring") -def get_output_types(f: Optional[FunctionType] = None) -> tuple[type]: +def get_output_types(f: Optional[FunctionType] = None) -> list[type]: """ Get the output type annotation of the parent caller function. Returns a list of types in case of a union, otherwise a list with one type. - """ # TODO: implement union type lists + """ try: return_type = (f or get_func_obj()).__annotations__["return"] if isinstance(return_type, UnionType): return return_type.__args__ # type: ignore else: - return (return_type,) + return [return_type] except KeyError: raise ValueError("The funcchain must have a return type annotation") @@ -69,7 +69,7 @@ def args_from_parent() -> list[tuple[str, type]]: def gather_signature( f: FunctionType, -) -> dict[str, str | list[tuple[str, type]] | tuple[type]]: +) -> dict[str, str | list[tuple[str, type]] | list[type]]: """ Gather the signature of the parent caller function. """ From c9631f72fdcb48f0e5bb7ac02a78636c4b49bece Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 244/451] =?UTF-8?q?=E2=9C=A8=20Add=20Jinja2=20template=20s?= =?UTF-8?q?upport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/prompt.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/funcchain/backend/prompt.py b/src/funcchain/backend/prompt.py index 266554f..4cb2131 100644 --- a/src/funcchain/backend/prompt.py +++ b/src/funcchain/backend/prompt.py @@ -1,6 +1,7 @@ from string import Formatter from typing import Any, Optional, Type +from jinja2 import Environment, meta from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage from langchain_core.prompts import ChatPromptTemplate @@ -22,7 +23,7 @@ def create_instruction_prompt( ) -> "HumanImageMessagePromptTemplate": template_format = _determine_format(instruction) - required_f_str_vars = _extract_fstring_vars(instruction) + required_f_str_vars = _extract_template_vars(instruction, template_format) _filter_fstring_vars(input_kwargs) @@ -81,9 +82,20 @@ def _determine_format( return "jinja2" if "{{" in instruction or "{%" in instruction else "f-string" +def _extract_template_vars( + template: str, + template_format: str, +) -> list[str]: + """ + Function to extract variables from a string template. + """ + if template_format == "jinja2": + return _extract_jinja_vars(template) + return _extract_fstring_vars(template) + + def _extract_fstring_vars(template: str) -> list[str]: """ - TODO: enable jinja2 check Function to extract f-string variables from a string. """ return [ @@ -93,6 +105,15 @@ def _extract_fstring_vars(template: str) -> list[str]: ] +def _extract_jinja_vars(template: str) -> list[str]: + """ + Function to extract variables from a Jinja2 template. + """ + env = Environment() + parsed_content = env.parse(template) + return list(meta.find_undeclared_variables(parsed_content)) + + def _filter_fstring_vars( input_kwargs: dict[str, Any], ) -> None: From 9866c4c4f47829cf962a88fc3172c8d0782f3ca1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 245/451] =?UTF-8?q?=E2=9C=A8=20Add=20local=20models=20path?= =?UTF-8?q?=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/backend/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/funcchain/backend/settings.py b/src/funcchain/backend/settings.py index 9c68a87..6557a46 100644 --- a/src/funcchain/backend/settings.py +++ b/src/funcchain/backend/settings.py @@ -41,6 +41,7 @@ class FuncchainSettings(BaseSettings): context_lenght: int = 8196 n_gpu_layers: int = 50 keep_loaded: bool = False + local_models_path: str = "./.models" def model_kwargs(self) -> dict: return { From 59c6b630712f1b25d20186b2b756e24f61034778 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 246/451] =?UTF-8?q?=F0=9F=94=80=20Update=20import=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/abilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/model/abilities.py b/src/funcchain/model/abilities.py index 82b94b6..e291a7a 100644 --- a/src/funcchain/model/abilities.py +++ b/src/funcchain/model/abilities.py @@ -1,7 +1,7 @@ from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage -from ..model.llm_overrides import ChatOllama +from .patches.ollama import ChatOllama verified_openai_function_models = [ "gpt-4", @@ -23,7 +23,7 @@ verified_ollama_vision_models = [ "llava", "bakllava", -] +] # TODO: llamacpp def gather_llm_type(llm: BaseChatModel, func_check: bool = True) -> str: From 2f915137ecac090ff65320da75039f8663e25663 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 247/451] =?UTF-8?q?=E2=9C=A8=20Add=20GGUF=20model=20suppor?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/defaults.py | 84 ++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/funcchain/model/defaults.py b/src/funcchain/model/defaults.py index 3857862..1f82c41 100644 --- a/src/funcchain/model/defaults.py +++ b/src/funcchain/model/defaults.py @@ -1,8 +1,76 @@ +from pathlib import Path from typing import Any from langchain_core.language_models import BaseChatModel from ..backend.settings import FuncchainSettings +from ..model.patches.llamacpp import ChatLlamaCpp + + +def get_gguf_model( + name: str, + label: str, + settings: FuncchainSettings, +) -> Path: + """ + Gather GGUF model from huggingface/TheBloke + + possible input: + - DiscoLM-mixtral-8x7b-v2-GGUF + - TheBloke/DiscoLM-mixtral-8x7b-v2 + - discolm-mixtral-8x7b-v2 + ... + + Raises ModelNotFound(name) error in case of no result. + """ + from huggingface_hub import hf_hub_download + + name = name.removesuffix("-GGUF") + label = "Q5_K_M" if label == "latest" else label + + model_path = Path(settings.local_models_path) + + if (p := model_path / f"{name.lower()}.{label}.gguf").exists(): + return p + + # check if available on huggingface + try: + # check local cache + + input( + f"Do you want to download this model from huggingface.co/TheBloke/{name}-GGUF ?\n" + "Press enter to continue." + ) + print("\033c") + print("Downloading model from huggingface...") + p = hf_hub_download( + repo_id=f"TheBloke/{name}-GGUF", + filename=f"{name.lower()}.{label}.gguf", + local_dir=model_path, + local_dir_use_symlinks=True, + ) + print("\033c") + return Path(p) + except Exception as e: + print(e) + raise ValueError(f"ModelNotFound: {name}.{label}") + + +def default_model_fallback( + settings: FuncchainSettings, + **model_kwargs: Any, +) -> ChatLlamaCpp: + """ + Give user multiple options for local models to download. + """ + if input("ModelNotFound: Do you want to download a local model instead?").lower().startswith("y"): + model_kwargs.update(settings.llamacpp_kwargs()) + return ChatLlamaCpp( + model_path=get_gguf_model("neural-chat-7b-v3-1", "Q4_K_M", settings).as_posix(), + **model_kwargs, + ) + print("Please select a model to use funcchain!") + exit(0) def univeral_model_selector( @@ -21,6 +89,7 @@ def univeral_model_selector( Examples: - "openai/gpt-3.5-turbo" - "anthropic/claude-2" + - "llamacpp/openchat-3.5-0106" (theblock gguf models) - "ollama/deepseek-llm-7b-chat" Supported: @@ -57,12 +126,25 @@ def univeral_model_selector( return ChatGooglePalm(**model_kwargs) case "ollama": - from .llm_overrides import ChatOllama + from .patches.ollama import ChatOllama model = model_kwargs.pop("model_name") model_kwargs.update(settings.ollama_kwargs()) return ChatOllama(model=model, **model_kwargs) + case "llamacpp" | "thebloke" | "gguf": + from .patches.llamacpp import ChatLlamaCpp + + model_kwargs.pop("model_name") + name, label = name.split(":") if ":" in name else (name, "latest") + model_path = get_gguf_model(name, label, settings).as_posix() + print("Using model:", model_path) + model_kwargs.update(settings.llamacpp_kwargs()) + return ChatLlamaCpp( + model_path=model_path, + **model_kwargs, + ) + except Exception as e: print("ERROR:", e) raise e From 86f2d2f72d21672f9815834c5ea9207a69d13380 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 248/451] =?UTF-8?q?=E2=9C=A8=20Add=20patches=20module=20in?= =?UTF-8?q?it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/patches/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/funcchain/model/patches/__init__.py diff --git a/src/funcchain/model/patches/__init__.py b/src/funcchain/model/patches/__init__.py new file mode 100644 index 0000000..e69de29 From bba4200b0018af3c583e1008006702207b7d32cf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 249/451] =?UTF-8?q?=F0=9F=A6=99=20Add=20LlamaCPP=20model?= =?UTF-8?q?=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/patches/llamacpp.py | 364 ++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 src/funcchain/model/patches/llamacpp.py diff --git a/src/funcchain/model/patches/llamacpp.py b/src/funcchain/model/patches/llamacpp.py new file mode 100644 index 0000000..6e8fd94 --- /dev/null +++ b/src/funcchain/model/patches/llamacpp.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any, Dict, Iterator, List, Optional, Union + +from langchain_core.callbacks.manager import CallbackManagerForLLMRun +from langchain_core.language_models import BaseChatModel, BaseLanguageModel +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, + ChatMessage, + HumanMessage, + SystemMessage, +) +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.pydantic_v1 import Field, root_validator +from langchain_core.utils import get_pydantic_field_names +from langchain_core.utils.utils import build_extra_kwargs + +logger = logging.getLogger(__name__) + + +try: + + class _LlamaCppCommon(BaseLanguageModel): + client: Any = Field(default=None, exclude=True) #: :meta private: + model_path: str + """The path to the Llama model file.""" + + lora_base: Optional[str] = None + """The path to the Llama LoRA base model.""" + + lora_path: Optional[str] = None + """The path to the Llama LoRA. If None, no LoRa is loaded.""" + + n_ctx: int = Field(4096, alias="n_ctx") + """Token context window.""" + + n_parts: int = Field(-1, alias="n_parts") + """Number of parts to split the model into. + If -1, the number of parts is automatically determined.""" + + seed: int = Field(-1, alias="seed") + """Seed. If -1, a random seed is used.""" + + f16_kv: bool = Field(True, alias="f16_kv") + """Use half-precision for key/value cache.""" + + logits_all: bool = Field(False, alias="logits_all") + """Return logits for all tokens, not just the last token.""" + + vocab_only: bool = Field(False, alias="vocab_only") + """Only load the vocabulary, no weights.""" + + use_mlock: bool = Field(False, alias="use_mlock") + """Force system to keep model in RAM.""" + + n_threads: Optional[int] = Field(None, alias="n_threads") + """Number of threads to use. + If None, the number of threads is automatically determined.""" + + n_batch: Optional[int] = Field(8, alias="n_batch") + """Number of tokens to process in parallel. + Should be a number between 1 and n_ctx.""" + + n_gpu_layers: Optional[int] = Field(42, alias="n_gpu_layers") + """Number of layers to be loaded into gpu memory. Default 42.""" + + suffix: Optional[str] = Field(None) + """A suffix to append to the generated text. If None, no suffix is appended.""" + + max_tokens: Optional[int] = 1024 + """The maximum number of tokens to generate.""" + + temperature: Optional[float] = 0.3 + """The temperature to use for sampling.""" + + top_p: Optional[float] = 0.95 + """The top-p value to use for sampling.""" + + logprobs: Optional[int] = Field(None) + """The number of logprobs to return. If None, no logprobs are returned.""" + + echo: Optional[bool] = False + """Whether to echo the prompt.""" + + stop: Optional[List[str]] = [] + """A list of strings to stop generation when encountered.""" + + repeat_penalty: Optional[float] = 1.1 + """The penalty to apply to repeated tokens.""" + + top_k: Optional[int] = 40 + """The top-k value to use for sampling.""" + + last_n_tokens_size: Optional[int] = 64 + """The number of tokens to look back when applying the repeat_penalty.""" + + use_mmap: Optional[bool] = True + """Whether to keep the model loaded in RAM""" + + rope_freq_scale: float = 1.0 + """Scale factor for rope sampling.""" + + rope_freq_base: float = 10000.0 + """Base frequency for rope sampling.""" + + model_kwargs: Dict[str, Any] = Field(default_factory=dict) + """Any additional parameters to pass to llama_cpp.Llama.""" + + streaming: bool = True + """Whether to stream the results, token by token.""" + + grammar_path: Optional[Union[str, Path]] = None + """ + grammar_path: Path to the .gbnf file that defines formal grammars + for constraining model outputs. For instance, the grammar can be used + to force the model to generate valid JSON or to speak exclusively in emojis. At most + one of grammar_path and grammar should be passed in. + """ + grammar: Optional[str] = None + """ + grammar: formal grammar for constraining model outputs. For instance, the grammar + can be used to force the model to generate valid JSON or to speak exclusively in + emojis. At most one of grammar_path and grammar should be passed in. + """ + + verbose: bool = False + """Print verbose output to stderr.""" + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate that llama-cpp-python library is installed.""" + try: + from llama_cpp import Llama, LlamaGrammar + except ImportError: + raise ImportError( + "Could not import llama-cpp-python library. " + "Please install the llama-cpp-python library to " + "use this embedding model: pip install llama-cpp-python" + ) + + model_path = values["model_path"] + model_param_names = [ + "rope_freq_scale", + "rope_freq_base", + "lora_path", + "lora_base", + "n_ctx", + "n_parts", + "seed", + "f16_kv", + "logits_all", + "vocab_only", + "use_mlock", + "n_threads", + "n_batch", + "use_mmap", + "last_n_tokens_size", + "verbose", + ] + model_params = {k: values[k] for k in model_param_names} + # For backwards compatibility, only include if non-null. + if values["n_gpu_layers"] is not None: + model_params["n_gpu_layers"] = values["n_gpu_layers"] + + model_params.update(values["model_kwargs"]) + + try: + values["client"] = Llama(model_path, **model_params) + except Exception as e: + raise ValueError(f"Could not load Llama model from path: {model_path}. " f"Received error {e}") + + if values["grammar"] and values["grammar_path"]: + grammar = values["grammar"] + grammar_path = values["grammar_path"] + raise ValueError( + "Can only pass in one of grammar and grammar_path. Received " f"{grammar=} and {grammar_path=}." + ) + elif isinstance(values["grammar"], str): + values["grammar"] = LlamaGrammar.from_string(values["grammar"]) + elif values["grammar_path"]: + values["grammar"] = LlamaGrammar.from_file(values["grammar_path"]) + else: + pass + return values + + @root_validator(pre=True) + def build_model_kwargs(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """Build extra kwargs from additional params that were passed in.""" + all_required_field_names = get_pydantic_field_names(cls) + extra = values.get("model_kwargs", {}) + values["model_kwargs"] = build_extra_kwargs(extra, values, all_required_field_names) + return values + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling llama_cpp.""" + params = { + "suffix": self.suffix, + "max_tokens": self.max_tokens, + "temperature": self.temperature, + "top_p": self.top_p, + "logprobs": self.logprobs, + "echo": self.echo, + "stop_sequences": self.stop, # key here is convention among LLM classes + "repeat_penalty": self.repeat_penalty, + "top_k": self.top_k, + } + if self.grammar: + params["grammar"] = self.grammar + return params + + @property + def _identifying_params(self) -> Dict[str, Any]: + """Get the identifying parameters.""" + return {**{"model_path": self.model_path}, **self._default_params} + + def _get_parameters(self, stop: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Performs sanity check, preparing parameters in format needed by llama_cpp. + + Args: + stop (Optional[List[str]]): List of stop sequences for llama_cpp. + + Returns: + Dictionary containing the combined parameters. + """ + + # Raise error if stop sequences are in both input and default params + if self.stop and stop is not None: + raise ValueError("`stop` found in both the input and default params.") + + params = self._default_params + + # llama_cpp expects the "stop" key not this, so we remove it: + params.pop("stop_sequences") + + # then sets it as configured, or default to an empty list: + params["stop"] = self.stop or stop or [] + + return params + + def get_num_tokens(self, text: str) -> int: + tokenized_text = self.client.tokenize(text.encode("utf-8")) + return len(tokenized_text) + + class ChatLlamaCpp(BaseChatModel, _LlamaCppCommon): + """llama.cpp chat model. + + To use, you should have the llama-cpp-python library installed, and provide the + path to the Llama model as a named parameter to the constructor. + Check out: https://github.com/abetlen/llama-cpp-python + + Example: + .. code-block:: python + + from funcchain._llms import ChatLlamaCpp + llm = ChatLlamaCpp(model_path="./path/to/model.gguf") + """ + + @property + def _llm_type(self) -> str: + """Return type of chat model.""" + return "llamacpp-chat" + + def _format_message_as_text(self, message: BaseMessage) -> str: + if isinstance(message, ChatMessage): + message_text = f"\n\n{message.role.capitalize()}: {message.content}" + elif isinstance(message, HumanMessage): + message_text = f"[INST] {message.content} [/INST]" + elif isinstance(message, AIMessage): + message_text = f"{message.content}" + elif isinstance(message, SystemMessage): + message_text = f"<> {message.content} <>" + else: + raise ValueError(f"Got unknown type {message}") + return message_text + + def _format_messages_as_text(self, messages: List[BaseMessage]) -> str: + return "\n".join([self._format_message_as_text(message) for message in messages]) + + def _stream_with_aggregation( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + verbose: bool = False, + **kwargs: Any, + ) -> ChatGenerationChunk: + final_chunk: Optional[ChatGenerationChunk] = None + for chunk in self._stream(messages, stop, **kwargs): + if final_chunk is None: + final_chunk = chunk + else: + final_chunk += chunk + if run_manager: + run_manager.on_llm_new_token( + chunk.text, + verbose=verbose, + ) + if final_chunk is None: + raise ValueError("No data received from llamacpp stream.") + + return final_chunk + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + """Call out to LlamaCpp's generation endpoint. + + Args: + messages: The list of base messages to pass into the model. + stop: Optional list of stop words to use when generating. + + Returns: + Chat generations from the model + + Example: + .. code-block:: python + + response = llamacpp([ + HumanMessage(content="Tell me about the history of AI") + ]) + """ + final_chunk = self._stream_with_aggregation( + messages, stop=stop, run_manager=run_manager, verbose=self.verbose, **kwargs + ) + chat_generation = ChatGeneration( + message=AIMessage(content=final_chunk.text), + generation_info=final_chunk.generation_info, + ) + return ChatResult(generations=[chat_generation]) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + params = {**self._get_parameters(stop), **kwargs} + prompt = self._format_messages_as_text(messages) + result = self.client(prompt=prompt, stream=True, **params) + for part in result: + logprobs = part["choices"][0].get("logprobs", None) + chunk = ChatGenerationChunk( + message=AIMessageChunk(content=part["choices"][0]["text"]), + generation_info={"logprobs": logprobs}, + ) + yield chunk + if run_manager: + run_manager.on_llm_new_token(token=chunk.text, verbose=self.verbose, log_probs=logprobs) +except ImportError: + + class ChatLlamaCpp: # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + raise ImportError("Please install langchain_community to use ChatLlamaCpp.") From d7b4b5bc6efaeafe77ba006ae823d71361f0a1b7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 250/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20ollama.py=20fil?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/patches/ollama.py | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/funcchain/model/patches/ollama.py diff --git a/src/funcchain/model/patches/ollama.py b/src/funcchain/model/patches/ollama.py new file mode 100644 index 0000000..eebebed --- /dev/null +++ b/src/funcchain/model/patches/ollama.py @@ -0,0 +1,51 @@ +from typing import Any, Dict, Optional + +from langchain_core.pydantic_v1 import validator + +try: + from langchain_community.chat_models import ChatOllama as _ChatOllama + + class ChatOllama(_ChatOllama): # type: ignore + grammar: Optional[str] = None + """ + The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the output. + """ + + @validator("grammar") + def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: + if v is not None and "root ::=" not in v: + raise ValueError("Grammar must contain a root rule.") + return v + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling Ollama.""" + return { + "model": self.model, + "format": self.format, + "options": { + "mirostat": self.mirostat, + "mirostat_eta": self.mirostat_eta, + "mirostat_tau": self.mirostat_tau, + "num_ctx": self.num_ctx, + "num_gpu": self.num_gpu, + "num_thread": self.num_thread, + "repeat_last_n": self.repeat_last_n, + "repeat_penalty": self.repeat_penalty, + "temperature": self.temperature, + "stop": self.stop, + "tfs_z": self.tfs_z, + "top_k": self.top_k, + "top_p": self.top_p, + "grammar": self.grammar, # added + }, + "system": self.system, + "template": self.template, + } + + +except ImportError: + + class ChatOllama: # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + raise ImportError("Please install langchain_community to use ChatOllama.") From 5a9192b605d381220a19b2fb192108b60fc3493a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 251/451] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20import,=20adjust?= =?UTF-8?q?=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/json_schema.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/funcchain/parser/json_schema.py b/src/funcchain/parser/json_schema.py index a193b21..2e452d4 100644 --- a/src/funcchain/parser/json_schema.py +++ b/src/funcchain/parser/json_schema.py @@ -9,8 +9,6 @@ from langchain_core.runnables import Runnable from pydantic import BaseModel, ValidationError -from ..syntax.output_types import CodeBlock as CodeBlock - M = TypeVar("M", bound=BaseModel) @@ -80,7 +78,7 @@ def retry_chain(self) -> Runnable: return compile_runnable( instruction="Retry parsing the output by fixing the error.", input_args=["output", "error"], - output_types=(self.pydantic_object,), + output_types=[self.pydantic_object], llm=self.retry_llm, settings_override={"retry_parse": self.retry - 1}, ) From b832431ed414de0ab4191fec09fa5336d140b08d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 252/451] =?UTF-8?q?=F0=9F=94=A7=20Adjust=20type=20annotati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/openai_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/parser/openai_functions.py b/src/funcchain/parser/openai_functions.py index 834174c..2a259e1 100644 --- a/src/funcchain/parser/openai_functions.py +++ b/src/funcchain/parser/openai_functions.py @@ -59,14 +59,14 @@ def retry_chain(self) -> Runnable: return compile_runnable( instruction="Retry parsing the output by fixing the error.", input_args=["output", "error"], - output_types=(self.pydantic_schema,), + output_types=[self.pydantic_schema], llm=self.retry_llm, settings_override={"retry_parse": self.retry - 1}, ) class RetryOpenAIFunctionPydanticUnionParser(BaseGenerationOutputParser[M]): - output_types: tuple[Type[M]] + output_types: list[type[M]] args_only: bool = False retry: int retry_llm: BaseChatModel | str | None = None From abf48ed76922362f85f1d36d30dcbb521e554f87 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 253/451] =?UTF-8?q?=E2=9C=A8=20Add=20primitive=20types=20p?= =?UTF-8?q?arser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/primitive_types.py | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/funcchain/parser/primitive_types.py diff --git a/src/funcchain/parser/primitive_types.py b/src/funcchain/parser/primitive_types.py new file mode 100644 index 0000000..58b8729 --- /dev/null +++ b/src/funcchain/parser/primitive_types.py @@ -0,0 +1,33 @@ +""" +Primitive Types Parser +""" +from typing import Generic, TypeVar + +from langchain_core.language_models import BaseChatModel +from pydantic import BaseModel, create_model + +from .json_schema import RetryJsonPydanticParser + +M = TypeVar("M", bound=BaseModel) + + +class RetryJsonPrimitiveTypeParser(RetryJsonPydanticParser, Generic[M]): + """ + Parse primitve types by wrapping them in a PydanticModel and parsing them. + Examples: int, float, bool, list[str], dict[str, int], Literal["a", "b", "c"], etc. + """ + + def __init__( + self, + primitive_type: type, + retry: int = 1, + retry_llm: BaseChatModel | str | None = None, + ) -> None: + super().__init__( + pydantic_object=create_model("ExtractPrimitiveType", value=(primitive_type, ...)), + retry=retry, + retry_llm=retry_llm, + ) + + def parse(self, text: str) -> M: + return super().parse(text).value From e90950955f18421f67314a109ff07e72278df8b3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 254/451] =?UTF-8?q?=E2=9C=A8=20Refactor=20parser=20selecti?= =?UTF-8?q?on=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/selector.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/funcchain/parser/selector.py b/src/funcchain/parser/selector.py index f6a8809..0345c30 100644 --- a/src/funcchain/parser/selector.py +++ b/src/funcchain/parser/selector.py @@ -1,14 +1,17 @@ +from enum import Enum +from typing import Literal, get_origin + from langchain_core.language_models import BaseChatModel from langchain_core.output_parsers import BaseGenerationOutputParser, BaseOutputParser, StrOutputParser from pydantic import BaseModel from ..parser.json_schema import RetryJsonPydanticParser, RetryJsonPydanticUnionParser -from ..parser.parsers import BoolOutputParser +from ..parser.primitive_types import RetryJsonPrimitiveTypeParser from ..syntax.output_types import ParserBaseModel def parser_for( - output_types: tuple[type], + output_types: list[type], retry: int, llm: BaseChatModel | str | None = None, ) -> BaseOutputParser | BaseGenerationOutputParser: @@ -23,8 +26,18 @@ def parser_for( if output_type is str: return StrOutputParser() - if output_type is bool: - return BoolOutputParser() + if ( + ((t := get_origin(output_type)) is int) + or (t is bool) + or (t is float) + or (t is list) + or (t is dict) + or (t is set) + or (t is tuple) + or (t is Literal) + or (t is Enum) + ): + return RetryJsonPrimitiveTypeParser(primitive_type=output_type, retry=retry, retry_llm=llm) if issubclass(output_type, ParserBaseModel): return output_type.output_parser() # type: ignore From cf73b4037480fc5eb8bf65332f6a298d523ee3d9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 255/451] =?UTF-8?q?=F0=9F=94=A5=20Simplify=20Signature=20m?= =?UTF-8?q?odel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/schema/signature.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/funcchain/schema/signature.py b/src/funcchain/schema/signature.py index 12e0c80..778aaba 100644 --- a/src/funcchain/schema/signature.py +++ b/src/funcchain/schema/signature.py @@ -1,14 +1,12 @@ -from typing import Any, Generic, TypeVar +from typing import Any from langchain_core.messages import BaseMessage from langchain_core.pydantic_v1 import BaseModel, Field from ..backend.settings import FuncchainSettings, settings -T = TypeVar("T", bound=Any) - -class Signature(BaseModel, Generic[T]): +class Signature(BaseModel): """ Fundamental structure of an executable prompt. """ @@ -19,15 +17,7 @@ class Signature(BaseModel, Generic[T]): input_args: list[tuple[str, type]] = Field(default_factory=list) """ List of input arguments for the prompt template. """ - # TODO collect types from input_args - # -> this would allow special prompt templating based on certain types - # -> e.g. BaseChatMessageHistory adds a history placeholder - # -> e.g. BaseChatModel overrides the default language model - # -> e.g. SettingsOverride overrides the default settings - # -> e.g. Callbacks adds custom callbacks - # -> e.g. SystemMessage adds a system message - - output_types: tuple[type[T]] + output_types: list[Any] """ Type to parse the output into. """ # todo: is history really needed? maybe this could be a background optimization From fe038c73726892047b8ac1fbf412ab562525b082 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 256/451] =?UTF-8?q?=F0=9F=94=84=20Update=20RouterModel=20o?= =?UTF-8?q?utput=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/components/router.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/syntax/components/router.py b/src/funcchain/syntax/components/router.py index 3d344bf..a5fc1a7 100644 --- a/src/funcchain/syntax/components/router.py +++ b/src/funcchain/syntax/components/router.py @@ -79,7 +79,7 @@ class RouterModel(BaseModel): return compile_runnable( instruction="Given the user request select the appropriate route.", input_args=["user_request", "routes"], # todo: optional images - output_types=(RouterModel,), + output_types=[RouterModel], context=self.history.messages if self.history else [], llm=self.llm, ) @@ -92,7 +92,7 @@ def _add_default_handler(self) -> None: | compile_runnable( instruction="{user_request}", input_args=["user_request"], - output_types=(str,), + output_types=[str], llm=self.llm, ) | RunnableLambda(lambda x: AIMessage(content=x)) From b192f3bd7fa37663ac22495ddfce3e5aaec47582 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 257/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20output=5Ftypes?= =?UTF-8?q?=20typing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py index 48eb5bc..47e742a 100644 --- a/src/funcchain/syntax/executable.py +++ b/src/funcchain/syntax/executable.py @@ -122,7 +122,7 @@ async def achain( def compile_runnable( *, instruction: str, - output_types: tuple[type[ChainOut]], + output_types: list[type[ChainOut]], input_args: list[str] = [], context: list = [], llm: BaseChatModel | str | None = None, From 50580e396ff6dee5081fe3e7f25a2d394413e1f5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 258/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20type=20annotati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/pydantic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/funcchain/utils/pydantic.py b/src/funcchain/utils/pydantic.py index 855ecfc..5d2c0b0 100644 --- a/src/funcchain/utils/pydantic.py +++ b/src/funcchain/utils/pydantic.py @@ -1,4 +1,4 @@ -from typing import Any, Type +from typing import Any from docstring_parser import parse from pydantic import BaseModel @@ -14,7 +14,7 @@ def _remove_a_key(d: dict, remove_key: str) -> None: _remove_a_key(d[key], remove_key) -def pydantic_to_functions(pydantic_type: Type[BaseModel]) -> dict[str, Any]: +def pydantic_to_functions(pydantic_type: type[BaseModel]) -> dict[str, Any]: schema = pydantic_type.model_json_schema() docstring = parse(pydantic_type.__doc__ or "") @@ -54,7 +54,7 @@ def pydantic_to_functions(pydantic_type: Type[BaseModel]) -> dict[str, Any]: def multi_pydantic_to_functions( - pydantic_types: tuple[Type[BaseModel]], + pydantic_types: list[type[BaseModel]], ) -> dict[str, Any]: functions: list[dict[str, Any]] = [ pydantic_to_functions(pydantic_type)["functions"][0] for pydantic_type in pydantic_types From 373212dda4047a79c6b4d65a760274eb28c90157 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 259/451] =?UTF-8?q?=E2=9C=A8=20Add=20jinja=5Ftest.py=20ske?= =?UTF-8?q?leton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/features/jinja_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/features/jinja_test.py diff --git a/tests/features/jinja_test.py b/tests/features/jinja_test.py new file mode 100644 index 0000000..e69de29 From f1826ee4cb9838ef3fa04d92501c38f2a0da2243 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 260/451] =?UTF-8?q?=E2=9C=A8=20Add=20retry=5Fvalidation=5F?= =?UTF-8?q?test.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/features/retry_validation_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/features/retry_validation_test.py diff --git a/tests/features/retry_validation_test.py b/tests/features/retry_validation_test.py new file mode 100644 index 0000000..e69de29 From 9994c3a1efcffc9c2e09abbcb4267e57ccec2d77 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 261/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20router=20component?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/features/router_component_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/features/router_component_test.py diff --git a/tests/features/router_component_test.py b/tests/features/router_component_test.py new file mode 100644 index 0000000..e69de29 From 794681efa551745d0c6eec22fdcc73f2db22905d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 262/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20streaming=5Ftest.p?= =?UTF-8?q?y=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/features/streaming_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/features/streaming_test.py diff --git a/tests/features/streaming_test.py b/tests/features/streaming_test.py new file mode 100644 index 0000000..e69de29 From 793396405cdb3356bf19073c8c644f76d73b3252 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 263/451] =?UTF-8?q?=E2=9C=A8=20Add=20llamacpp=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/models/llamacpp_test.py | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/models/llamacpp_test.py diff --git a/tests/models/llamacpp_test.py b/tests/models/llamacpp_test.py new file mode 100644 index 0000000..723977f --- /dev/null +++ b/tests/models/llamacpp_test.py @@ -0,0 +1,86 @@ +import pytest +from funcchain import Image, chain, settings +from pydantic import BaseModel, Field + + +class Task(BaseModel): + description: str + difficulty: int + + +class TodoList(BaseModel): + tasks: list[Task] + + +def todo_list(job_title: str) -> TodoList: + """ + Create a todo list for a perfect day for the given job. + """ + return chain() + + +@pytest.mark.skip_on_actions +def test_openhermes() -> None: + settings.llm = "llamacpp/Nous-Hermes-2-SOLAR-10.7B" + + assert isinstance( + todo_list("software engineer"), + TodoList, + ) + + +@pytest.mark.skip_on_actions +def test_neural_chat() -> None: + settings.llm = "llamacpp/openchat-3.5-1210:Q3_K_M" + + assert isinstance( + todo_list("ai engineer"), + TodoList, + ) + + +class Analysis(BaseModel): + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + + +def analyse(image: Image) -> Analysis: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + + +# TODO: vision support +# @pytest.mark.skip_on_actions +# def test_vision() -> None: +# settings.llm = "llamacpp/bakllava" + +# assert isinstance( +# analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), +# Analysis, +# ) # todo check actual output + + +# TODO: Test union types +# def test_union_types() -> None: +# ... + + +def test_model_search_failure() -> None: + settings.llm = "llamacpp/neural-chat-ultra-mega" + + try: + todo_list("software engineer") + except Exception: + assert True + else: + assert False, "Model should not be found" + + +if __name__ == "__main__": + test_openhermes() + test_neural_chat() + # test_vision() + test_model_search_failure() From aa32996feade0b28f581c8a643d8900624efe83c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 264/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20ollama=5Ftest.p?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/models/ollama_test.py | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/models/ollama_test.py diff --git a/tests/models/ollama_test.py b/tests/models/ollama_test.py new file mode 100644 index 0000000..1cf0546 --- /dev/null +++ b/tests/models/ollama_test.py @@ -0,0 +1,85 @@ +import pytest +from funcchain import Image, chain, settings +from pydantic import BaseModel, Field + + +class Task(BaseModel): + description: str + difficulty: int + + +class TodoList(BaseModel): + tasks: list[Task] + + +def todo_list(job_title: str) -> TodoList: + """ + Create a todo list for a perfect day for the given job. + """ + return chain() + + +@pytest.mark.skip_on_actions +def test_openhermes() -> None: + settings.llm = "ollama/openhermes2.5-mistral" + + assert isinstance( + todo_list("software engineer"), + TodoList, + ) + + +@pytest.mark.skip_on_actions +def test_neural_chat() -> None: + settings.llm = "ollama/openchat" + + assert isinstance( + todo_list("ai engineer"), + TodoList, + ) + + +class Analysis(BaseModel): + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + + +def analyse(image: Image) -> Analysis: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + + +@pytest.mark.skip_on_actions +def test_vision() -> None: + settings.llm = "ollama/bakllava" + + assert isinstance( + analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), + Analysis, + ) # todo check actual output + + +# TODO: Test union types +# def test_union_types() -> None: +# ... + + +def test_model_search_failure() -> None: + settings.llm = "ollama/neural-chat-ultra-mega" + + try: + todo_list("software engineer") + except Exception: + assert True + else: + assert False, "Model should not be found" + + +if __name__ == "__main__": + test_openhermes() + test_neural_chat() + # test_vision() + test_model_search_failure() From 732f91d449a1c8f6b35b628188487eb4259b4bc3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 265/451] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20openai=5Ftest.p?= =?UTF-8?q?y=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/models/openai_test.py | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/models/openai_test.py diff --git a/tests/models/openai_test.py b/tests/models/openai_test.py new file mode 100644 index 0000000..efbabed --- /dev/null +++ b/tests/models/openai_test.py @@ -0,0 +1,64 @@ +from funcchain import chain, settings +from pydantic import BaseModel, Field + + +class Task(BaseModel): + name: str + description: str + + +class TodoList(BaseModel): + tasks: list[Task] + + +def todo_list(job_title: str) -> TodoList: + """ + Create a todo list for a perfect day for the given job. + """ + return chain() + + +def test_gpt_35_turbo() -> None: + settings.llm = "openai/gpt-3.5-turbo" + + assert isinstance( + todo_list("software engineer"), + TodoList, + ) + + +def test_gpt4() -> None: + settings.llm = "openai/gpt-4" + + assert isinstance( + todo_list("software engineer"), + TodoList, + ) + + +def test_vision() -> None: + from funcchain import Image + + settings.llm = "openai/gpt-4-vision-preview" + + class Analysis(BaseModel): + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + + def analyse(image: Image) -> Analysis: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + + assert isinstance( + analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), + Analysis, + ) + + +if __name__ == "__main__": + test_gpt_35_turbo() + test_gpt4() + test_vision() From a7d52ad2d2c0ded1648d0dff8aa8737aeccc1d8a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:33 +0000 Subject: [PATCH 266/451] =?UTF-8?q?=F0=9F=94=8D=20Improve=20example=20test?= =?UTF-8?q?=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/run_examples_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/run_examples_test.py b/tests/run_examples_test.py index 9eb75f0..feb8b3a 100644 --- a/tests/run_examples_test.py +++ b/tests/run_examples_test.py @@ -5,13 +5,16 @@ async def run_script(file_path: str) -> tuple[str, int | None, bytes, bytes]: """Run a single script and return the result.""" + print(f"Running {file_path}...") process = await asyncio.create_subprocess_exec("python", file_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = await process.communicate() + print(f"Finished {file_path}.") + print(stdout.decode(), stderr.decode()) return file_path, process.returncode, stdout, stderr async def main() -> None: - files: list[str] = glob.glob("example/**/*.py", recursive=True) + files: list[str] = glob.glob("examples/**/*.py", recursive=True) tasks: list = [run_script(file) for file in files] results: list[tuple[str, int | None, bytes, bytes]] = await asyncio.gather(*tasks) @@ -24,8 +27,9 @@ async def main() -> None: def test_examples() -> None: - asyncio.run(main()) + # asyncio.run(main()) + ... if __name__ == "__main__": - test_examples() + asyncio.run(main()) From c140eff2377faffdf24cc8e8f11f40e8f944d373 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:44 +0000 Subject: [PATCH 267/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20llm=5F?= =?UTF-8?q?overrides.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/llm_overrides.py | 51 ---------------------------- 1 file changed, 51 deletions(-) delete mode 100644 src/funcchain/model/llm_overrides.py diff --git a/src/funcchain/model/llm_overrides.py b/src/funcchain/model/llm_overrides.py deleted file mode 100644 index eebebed..0000000 --- a/src/funcchain/model/llm_overrides.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Any, Dict, Optional - -from langchain_core.pydantic_v1 import validator - -try: - from langchain_community.chat_models import ChatOllama as _ChatOllama - - class ChatOllama(_ChatOllama): # type: ignore - grammar: Optional[str] = None - """ - The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the output. - """ - - @validator("grammar") - def _validate_grammar(cls, v: Optional[str]) -> Optional[str]: - if v is not None and "root ::=" not in v: - raise ValueError("Grammar must contain a root rule.") - return v - - @property - def _default_params(self) -> Dict[str, Any]: - """Get the default parameters for calling Ollama.""" - return { - "model": self.model, - "format": self.format, - "options": { - "mirostat": self.mirostat, - "mirostat_eta": self.mirostat_eta, - "mirostat_tau": self.mirostat_tau, - "num_ctx": self.num_ctx, - "num_gpu": self.num_gpu, - "num_thread": self.num_thread, - "repeat_last_n": self.repeat_last_n, - "repeat_penalty": self.repeat_penalty, - "temperature": self.temperature, - "stop": self.stop, - "tfs_z": self.tfs_z, - "top_k": self.top_k, - "top_p": self.top_p, - "grammar": self.grammar, # added - }, - "system": self.system, - "template": self.template, - } - - -except ImportError: - - class ChatOllama: # type: ignore - def __init__(self, *args: Any, **kwargs: Any) -> None: - raise ImportError("Please install langchain_community to use ChatOllama.") From ce79f1c09396a0bda9e3b35e36fe2d64f41bd4c6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:44 +0000 Subject: [PATCH 268/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20ollama?= =?UTF-8?q?=5Ftest.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/ollama_test.py | 85 -------------------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 tests/ollama_test.py diff --git a/tests/ollama_test.py b/tests/ollama_test.py deleted file mode 100644 index 1cf0546..0000000 --- a/tests/ollama_test.py +++ /dev/null @@ -1,85 +0,0 @@ -import pytest -from funcchain import Image, chain, settings -from pydantic import BaseModel, Field - - -class Task(BaseModel): - description: str - difficulty: int - - -class TodoList(BaseModel): - tasks: list[Task] - - -def todo_list(job_title: str) -> TodoList: - """ - Create a todo list for a perfect day for the given job. - """ - return chain() - - -@pytest.mark.skip_on_actions -def test_openhermes() -> None: - settings.llm = "ollama/openhermes2.5-mistral" - - assert isinstance( - todo_list("software engineer"), - TodoList, - ) - - -@pytest.mark.skip_on_actions -def test_neural_chat() -> None: - settings.llm = "ollama/openchat" - - assert isinstance( - todo_list("ai engineer"), - TodoList, - ) - - -class Analysis(BaseModel): - description: str = Field(description="A description of the image") - objects: list[str] = Field(description="A list of objects found in the image") - - -def analyse(image: Image) -> Analysis: - """ - Analyse the image and extract its - theme, description and objects. - """ - return chain() - - -@pytest.mark.skip_on_actions -def test_vision() -> None: - settings.llm = "ollama/bakllava" - - assert isinstance( - analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), - Analysis, - ) # todo check actual output - - -# TODO: Test union types -# def test_union_types() -> None: -# ... - - -def test_model_search_failure() -> None: - settings.llm = "ollama/neural-chat-ultra-mega" - - try: - todo_list("software engineer") - except Exception: - assert True - else: - assert False, "Model should not be found" - - -if __name__ == "__main__": - test_openhermes() - test_neural_chat() - # test_vision() - test_model_search_failure() From 6a97c59c145e785bf683d6829abacb17c6d77b04 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 26 Jan 2024 19:26:44 +0000 Subject: [PATCH 269/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20openai?= =?UTF-8?q?=5Ftest.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/openai_test.py | 64 -------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 tests/openai_test.py diff --git a/tests/openai_test.py b/tests/openai_test.py deleted file mode 100644 index efbabed..0000000 --- a/tests/openai_test.py +++ /dev/null @@ -1,64 +0,0 @@ -from funcchain import chain, settings -from pydantic import BaseModel, Field - - -class Task(BaseModel): - name: str - description: str - - -class TodoList(BaseModel): - tasks: list[Task] - - -def todo_list(job_title: str) -> TodoList: - """ - Create a todo list for a perfect day for the given job. - """ - return chain() - - -def test_gpt_35_turbo() -> None: - settings.llm = "openai/gpt-3.5-turbo" - - assert isinstance( - todo_list("software engineer"), - TodoList, - ) - - -def test_gpt4() -> None: - settings.llm = "openai/gpt-4" - - assert isinstance( - todo_list("software engineer"), - TodoList, - ) - - -def test_vision() -> None: - from funcchain import Image - - settings.llm = "openai/gpt-4-vision-preview" - - class Analysis(BaseModel): - description: str = Field(description="A description of the image") - objects: list[str] = Field(description="A list of objects found in the image") - - def analyse(image: Image) -> Analysis: - """ - Analyse the image and extract its - theme, description and objects. - """ - return chain() - - assert isinstance( - analyse(Image.from_file("examples/assets/old_chinese_temple.jpg")), - Analysis, - ) - - -if __name__ == "__main__": - test_gpt_35_turbo() - test_gpt4() - test_vision() From 473de9729245441e551f03d1388de05acc6af5df Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:31:18 +0000 Subject: [PATCH 270/451] =?UTF-8?q?=F0=9F=A7=AA=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dspy.todo | 9 --------- tests/features/primitive_types.py | 0 tests/models/llamacpp_test.py | 2 +- tests/retry_parsing_test.py | 1 - tests/router_test.py | 1 - 5 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 dspy.todo create mode 100644 tests/features/primitive_types.py delete mode 100644 tests/retry_parsing_test.py delete mode 100644 tests/router_test.py diff --git a/dspy.todo b/dspy.todo deleted file mode 100644 index 4e64b62..0000000 --- a/dspy.todo +++ /dev/null @@ -1,9 +0,0 @@ -To make this possible I need to: -- rewrite funcchain to be more dynamic - - modular backends - - unify syntax and schemas - - invent new syntax for special dspy modules (COT, FewShot, ...) - - seperate string and structured types logic - - - -booth are cutting edge ml libraries diff --git a/tests/features/primitive_types.py b/tests/features/primitive_types.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/llamacpp_test.py b/tests/models/llamacpp_test.py index 723977f..cb5f38c 100644 --- a/tests/models/llamacpp_test.py +++ b/tests/models/llamacpp_test.py @@ -31,7 +31,7 @@ def test_openhermes() -> None: @pytest.mark.skip_on_actions def test_neural_chat() -> None: - settings.llm = "llamacpp/openchat-3.5-1210:Q3_K_M" + settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" assert isinstance( todo_list("ai engineer"), diff --git a/tests/retry_parsing_test.py b/tests/retry_parsing_test.py deleted file mode 100644 index 044a482..0000000 --- a/tests/retry_parsing_test.py +++ /dev/null @@ -1 +0,0 @@ -# todo diff --git a/tests/router_test.py b/tests/router_test.py deleted file mode 100644 index 044a482..0000000 --- a/tests/router_test.py +++ /dev/null @@ -1 +0,0 @@ -# todo From fd3905d3124f3d5e89a43843005470a83ea76289 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:31:27 +0000 Subject: [PATCH 271/451] =?UTF-8?q?=F0=9F=94=84=20Update=20model=20referen?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/llamacpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llamacpp.py b/examples/llamacpp.py index 29c90c5..2a8cdfa 100644 --- a/examples/llamacpp.py +++ b/examples/llamacpp.py @@ -19,7 +19,7 @@ def analyze(text: str) -> SentimentAnalysis: if __name__ == "__main__": # set global llm - settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" + settings.llm = "llamacpp/nous-hermes-2-solar-10.7b" # log tokens as stream to console settings.console_stream = True From fd725ae76c8ef736337461fe9773c9934652f98c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:31:27 +0000 Subject: [PATCH 272/451] =?UTF-8?q?=E2=9C=A8=20Add=20roadmap=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmap.todo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roadmap.todo b/roadmap.todo index cdb4dd8..16ebb60 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -32,3 +32,5 @@ COMING SOON: [ ] - parallel function calling (8h) [ ] - FuncUnion and str output (6h) + +[ ] - think of new syntax for special dspy modules (COT, FewShot, ...) \ No newline at end of file From 121a011913edba3884ed1a332ab858b2339c22be Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:31:27 +0000 Subject: [PATCH 273/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20image=20handling?= =?UTF-8?q?=20to=20Ollama?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/patches/ollama.py | 85 ++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/src/funcchain/model/patches/ollama.py b/src/funcchain/model/patches/ollama.py index eebebed..31792e7 100644 --- a/src/funcchain/model/patches/ollama.py +++ b/src/funcchain/model/patches/ollama.py @@ -1,11 +1,14 @@ -from typing import Any, Dict, Optional +import base64 +from typing import Any, Dict, Optional, Union +import requests # type: ignore +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage from langchain_core.pydantic_v1 import validator try: from langchain_community.chat_models import ChatOllama as _ChatOllama - class ChatOllama(_ChatOllama): # type: ignore + class ChatOllama(_ChatOllama): grammar: Optional[str] = None """ The [GBNF](https://github.com/ggerganov/llama.cpp/tree/master/grammars) grammar used to constrain the output. @@ -43,6 +46,84 @@ def _default_params(self) -> Dict[str, Any]: "template": self.template, } + def _convert_messages_to_ollama_messages( + self, messages: list[BaseMessage] + ) -> list[dict[str, Union[str, list[str]]]]: + ollama_messages = [] + for message in messages: + role = "" + if isinstance(message, HumanMessage): + role = "user" + elif isinstance(message, AIMessage): + role = "assistant" + elif isinstance(message, SystemMessage): + role = "system" + else: + raise ValueError("Received unsupported message type for Ollama.") + + content = "" + images = [] + if isinstance(message.content, str): + content = message.content + else: + image_urls = [] + for content_part in message.content: + if isinstance(content_part, str): + content += f"\n{content_part}" + elif content_part.get("type") == "text": + content += f"\n{content_part['text']}" + elif content_part.get("type") == "image_url": + if isinstance(content_part.get("image_url"), str): + if content_part["image_url"].startswith("data:"): + image_url_components = content_part["image_url"].split(",") + # Support data:image/jpeg;base64, format + # and base64 strings + if len(image_url_components) > 1: + images.append(image_url_components[1]) + else: + images.append(image_url_components[0]) + else: + image_urls.append(content_part["image_url"]) + else: + if isinstance(content_part.get("image_url"), dict): + if content_part["image_url"]["url"].startswith("data:"): + image_url_components = content_part["image_url"]["url"].split(",") + # Support data:image/jpeg;base64, format + # and base64 strings + if len(image_url_components) > 1: + images.append(image_url_components[1]) + else: + images.append(image_url_components[0]) + else: + image_urls.append(content_part["image_url"]["url"]) + else: + raise ValueError("Unsupported message content type.") + else: + raise ValueError( + "Unsupported message content type. " + "Must either have type 'text' or type 'image_url' " + "with a string 'image_url' field." + ) + # download images and append base64 strings + if image_urls: + for image_url in image_urls: + response = requests.get(image_url) + if response.status_code == 200: + image = response.content + images.append(base64.b64encode(image).decode("utf-8")) + else: + raise ValueError(f"Failed to download image from {image_url}.") + + ollama_messages.append( + { + "role": role, + "content": content, + "images": images, + } + ) + + return ollama_messages # type: ignore + except ImportError: From 093809650b9be0ce925d8bdc7254dc267248b827 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:31:27 +0000 Subject: [PATCH 274/451] =?UTF-8?q?=E2=9C=A8=20Add=20list=20parsing=20supp?= =?UTF-8?q?ort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/selector.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/funcchain/parser/selector.py b/src/funcchain/parser/selector.py index 0345c30..132b771 100644 --- a/src/funcchain/parser/selector.py +++ b/src/funcchain/parser/selector.py @@ -26,10 +26,12 @@ def parser_for( if output_type is str: return StrOutputParser() + # TODO: write tests for each of these cases if ( - ((t := get_origin(output_type)) is int) - or (t is bool) - or (t is float) + (output_type is bool) + or (output_type is int) + or (output_type is float) + or ((t := get_origin(output_type)) is list) or (t is list) or (t is dict) or (t is set) From 8e7086681eff7260950f31e292ac9ca68fdd0822 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:31:27 +0000 Subject: [PATCH 275/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20type=20annotati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/utils/memory.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/funcchain/utils/memory.py b/src/funcchain/utils/memory.py index cfe5d8e..a0c97be 100644 --- a/src/funcchain/utils/memory.py +++ b/src/funcchain/utils/memory.py @@ -1,7 +1,5 @@ """langchain_community.chat_message_histories.in_memory.ChatMessageHistory""" -from typing import List - from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.messages import BaseMessage from langchain_core.pydantic_v1 import BaseModel, Field @@ -13,7 +11,7 @@ class ChatMessageHistory(BaseChatMessageHistory, BaseModel): Stores messages in an in memory list. """ - messages: List[BaseMessage] = Field(default_factory=list) + messages: list[BaseMessage] = Field(default_factory=list) def add_message(self, message: BaseMessage) -> None: """Add a self-created message to the store""" From e24dc02338faa67bb3e3a9e9dad6ba5f4b934e03 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:32:44 +0000 Subject: [PATCH 276/451] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20v0.2.0-alpha.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a30c419..a608725 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcchain" -version = "0.2.0-alpha.4" +version = "0.2.0-alpha.5" description = "🔖 write prompts as python functions" authors = [ { name = "Shroominic", email = "contact@shroominic.com" } From 6716eddaa2eb0dc334a5327bf26bd00e8fd35c43 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:51:21 +0000 Subject: [PATCH 277/451] =?UTF-8?q?=F0=9F=94=A7=20fix=20langsmith=20run=20?= =?UTF-8?q?names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/syntax/executable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/funcchain/syntax/executable.py b/src/funcchain/syntax/executable.py index 47e742a..4bab684 100644 --- a/src/funcchain/syntax/executable.py +++ b/src/funcchain/syntax/executable.py @@ -59,7 +59,7 @@ def chain( settings=settings, ) chain: Runnable[dict[str, Any], Any] = compile_chain(sig, temp_images) - result = chain.invoke(input_kwargs, {"run_name": get_parent_frame(3).function, "callbacks": callbacks}) + result = chain.invoke(input_kwargs, {"run_name": get_parent_frame(2).function, "callbacks": callbacks}) if memory and isinstance(result, str): # TODO: function calls? @@ -107,7 +107,7 @@ async def achain( settings=settings, ) chain: Runnable[dict[str, str], Any] = compile_chain(sig, temp_images) - result = await chain.ainvoke(input_kwargs, {"run_name": get_parent_frame(5).function, "callbacks": callbacks}) + result = await chain.ainvoke(input_kwargs, {"run_name": get_parent_frame(2).function, "callbacks": callbacks}) if memory and isinstance(result, str): # TODO: function calls? From cd58bfb178ff0b3b03e665210f49b08d8332482c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 11:57:49 +0000 Subject: [PATCH 278/451] =?UTF-8?q?=F0=9F=93=9D=20custom=20model=20display?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/custom_model_display.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 examples/custom_model_display.py diff --git a/examples/custom_model_display.py b/examples/custom_model_display.py new file mode 100644 index 0000000..1bfe69a --- /dev/null +++ b/examples/custom_model_display.py @@ -0,0 +1,27 @@ +from funcchain import chain +from pydantic import BaseModel + + +class Task(BaseModel): + name: str + description: str + difficulty: int + + def __str__(self) -> str: + return f"{self.name}\n - {self.description}\n - Difficulty: {self.difficulty}" + + +def plan_task(task: Task) -> str: + """ + Based on the task infos, plan the task step by step. + """ + return chain() + + +if __name__ == "__main__": + task = Task( + name="Do Laundry", + description="Collect and wash all the dirty clothes.", + difficulty=4, + ) + print(plan_task(task)) From a5b701401d053fcfcdaf153159c6d8d36c4db03d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 12:54:11 +0000 Subject: [PATCH 279/451] =?UTF-8?q?=F0=9F=94=A7=20fix=20code=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/features/{primitive_types.py => primitive_types_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/features/{primitive_types.py => primitive_types_test.py} (100%) diff --git a/tests/features/primitive_types.py b/tests/features/primitive_types_test.py similarity index 100% rename from tests/features/primitive_types.py rename to tests/features/primitive_types_test.py From 60edca6909cac7c1082ccfc29bb35b4fc6ce3379 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 27 Jan 2024 12:54:18 +0000 Subject: [PATCH 280/451] =?UTF-8?q?=F0=9F=93=9D=20update=20todos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo.md | 35 +++++++++++++++++++++++++++++++++++ roadmap.todo | 6 +++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 docs/todo.md diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..ab209d3 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,35 @@ +# TODOs for writing the documentation + +- [ ] convert MODELS.md into a seperate docs page + +- [ ] explaination for important examples + +- [ ] features breakdown + +- [ ] list all different output types/parsers + +- [ ] list all different input argument types + +- [ ] section about prompt writing and jinja syntax + examples how prompts are compiled + +- [ ] langchain compatibility explanation and examples + +- [ ] async/await explanation and examples + +- [ ] explaination of codebase structure and how to contribute + +- [ ] bounties/todos for contributors + +- [ ] union types/ error handling + +- [ ] page about local models (llamacpp/ollama) + +- [ ] document vision support + +- [ ] how to scale/structure a complex codebase using funcchain + +- [ ] funcchain settings + +- [ ] streaming support and examples + +- [ ] explain signature schema diff --git a/roadmap.todo b/roadmap.todo index 16ebb60..76a702a 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -1,8 +1,8 @@ -IMPORTANT: +IMPORTANT NOW: [ ] - enable union type without function calling (6h) [ ] - improve docs (8h) -COMING SOON: +TODO: [ ] - pydantic model streaming (6h) @@ -33,4 +33,4 @@ COMING SOON: [ ] - FuncUnion and str output (6h) -[ ] - think of new syntax for special dspy modules (COT, FewShot, ...) \ No newline at end of file +[ ] - think of new syntax for special dspy modules (COT, FewShot, ...) From b39c8274ad4eece5d91e8455aef8e4893c58edeb Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 00:26:17 +0000 Subject: [PATCH 281/451] =?UTF-8?q?=F0=9F=94=96=20todo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/features/jinja_test.py | 1 + tests/features/primitive_types_test.py | 1 + tests/features/retry_validation_test.py | 1 + tests/features/router_component_test.py | 1 + tests/features/streaming_test.py | 1 + 5 files changed, 5 insertions(+) diff --git a/tests/features/jinja_test.py b/tests/features/jinja_test.py index e69de29..15d61cb 100644 --- a/tests/features/jinja_test.py +++ b/tests/features/jinja_test.py @@ -0,0 +1 @@ +# TODO: implement tests for jinja2 templates diff --git a/tests/features/primitive_types_test.py b/tests/features/primitive_types_test.py index e69de29..cc49d71 100644 --- a/tests/features/primitive_types_test.py +++ b/tests/features/primitive_types_test.py @@ -0,0 +1 @@ +# TODO: implement tests for primitive types diff --git a/tests/features/retry_validation_test.py b/tests/features/retry_validation_test.py index e69de29..7ec78e1 100644 --- a/tests/features/retry_validation_test.py +++ b/tests/features/retry_validation_test.py @@ -0,0 +1 @@ +# TODO: implement tests for retry validation diff --git a/tests/features/router_component_test.py b/tests/features/router_component_test.py index e69de29..3c52739 100644 --- a/tests/features/router_component_test.py +++ b/tests/features/router_component_test.py @@ -0,0 +1 @@ +# TODO: implement tests for router component diff --git a/tests/features/streaming_test.py b/tests/features/streaming_test.py index e69de29..e978ce3 100644 --- a/tests/features/streaming_test.py +++ b/tests/features/streaming_test.py @@ -0,0 +1 @@ +# TODO: implement tests for streaming From 82aaf82553b91e6cb5b3c0a1d15864bf7f0870d6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:04:51 +0000 Subject: [PATCH 282/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20CONTRIBUTING=20gui?= =?UTF-8?q?delines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b059829 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# Contributing + +To contribute, clone the repo and run: + +```bash +./dev_setup.sh +``` + +You should not run unstrusted scripts so ask ChatGPT to explain what the contents of this script do! + +This will install and setup your development environment using [rye](https://rye-up.com) or pip. From 1601577d782f5fd388ce8555d40d7f8a1b73665d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:04:51 +0000 Subject: [PATCH 283/451] =?UTF-8?q?=E2=9C=A8=20Add=20insert=20examples=20d?= =?UTF-8?q?oc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/insert-examples.todo | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/features/insert-examples.todo diff --git a/docs/features/insert-examples.todo b/docs/features/insert-examples.todo new file mode 100644 index 0000000..e69de29 From 075ffa29e7cae7d9b146d269a718b61017824526 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:04:51 +0000 Subject: [PATCH 284/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20todo.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo.md | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 docs/todo.md diff --git a/docs/todo.md b/docs/todo.md deleted file mode 100644 index ab209d3..0000000 --- a/docs/todo.md +++ /dev/null @@ -1,35 +0,0 @@ -# TODOs for writing the documentation - -- [ ] convert MODELS.md into a seperate docs page - -- [ ] explaination for important examples - -- [ ] features breakdown - -- [ ] list all different output types/parsers - -- [ ] list all different input argument types - -- [ ] section about prompt writing and jinja syntax + examples how prompts are compiled - -- [ ] langchain compatibility explanation and examples - -- [ ] async/await explanation and examples - -- [ ] explaination of codebase structure and how to contribute - -- [ ] bounties/todos for contributors - -- [ ] union types/ error handling - -- [ ] page about local models (llamacpp/ollama) - -- [ ] document vision support - -- [ ] how to scale/structure a complex codebase using funcchain - -- [ ] funcchain settings - -- [ ] streaming support and examples - -- [ ] explain signature schema From c95a473e7c5069d74e48d105ad78a67288b6a7cf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:04:51 +0000 Subject: [PATCH 285/451] =?UTF-8?q?=F0=9F=94=80=20Reorder=20roadmap=20task?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmap.todo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roadmap.todo b/roadmap.todo index 76a702a..bc4b009 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -1,9 +1,10 @@ IMPORTANT NOW: -[ ] - enable union type without function calling (6h) [ ] - improve docs (8h) TODO: +[ ] - enable union type without function calling (6h) + [ ] - pydantic model streaming (6h) [ ] - cookbooks folder with jupyter notebook tutorials (8h) From 74caf52ce45cc46ade04e67dfd72363cdc52aaae Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:25 +0000 Subject: [PATCH 286/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20async=20documentat?= =?UTF-8?q?ion=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/async.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/advanced/async.md diff --git a/docs/advanced/async.md b/docs/advanced/async.md new file mode 100644 index 0000000..f82740c --- /dev/null +++ b/docs/advanced/async.md @@ -0,0 +1,13 @@ +# Async + +## Why and how to use using async? + +## How to use async? + +## Async Examples + +## Async in FuncChain + +## Async in LangChain + +## Async Streaming From a2bfea0a9242b2edbfac02ccfef0c05eee590637 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:25 +0000 Subject: [PATCH 287/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20codebase=20scaling?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/codebase-scaling.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/advanced/codebase-scaling.md diff --git a/docs/advanced/codebase-scaling.md b/docs/advanced/codebase-scaling.md new file mode 100644 index 0000000..6c52d8d --- /dev/null +++ b/docs/advanced/codebase-scaling.md @@ -0,0 +1,5 @@ +# Codebase Scaling + +## Multi file projects + +## Structure From b38d50b232d5e75f0f4eb0c08e8ada9bd0044834 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:25 +0000 Subject: [PATCH 288/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20runnables=20docume?= =?UTF-8?q?ntation=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/runnables.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/advanced/runnables.md diff --git a/docs/advanced/runnables.md b/docs/advanced/runnables.md new file mode 100644 index 0000000..026fb01 --- /dev/null +++ b/docs/advanced/runnables.md @@ -0,0 +1,5 @@ +# runnables + +## LangChain Expression Language (LCEL) + +## Streaming, Parallel, Async and From b1f2a5810aa4aa95c616faae57e23411f172737a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:25 +0000 Subject: [PATCH 289/451] =?UTF-8?q?=E2=9C=A8=20Add=20signature=20documenta?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/signature.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/advanced/signature.md diff --git a/docs/advanced/signature.md b/docs/advanced/signature.md new file mode 100644 index 0000000..8b806c5 --- /dev/null +++ b/docs/advanced/signature.md @@ -0,0 +1,5 @@ +# Signature + +## Compilation + +## Schema From b09655686934b4492df7c7834aff3676abf612c7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:25 +0000 Subject: [PATCH 290/451] =?UTF-8?q?=F0=9F=93=84=20Add=20stream-parsing=20d?= =?UTF-8?q?ocumentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/stream-parsing.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/advanced/stream-parsing.md diff --git a/docs/advanced/stream-parsing.md b/docs/advanced/stream-parsing.md new file mode 100644 index 0000000..e69de29 From aff7a06e919442ca90fb07eb8de5b36047cdd7e6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:25 +0000 Subject: [PATCH 291/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20codebase=20structu?= =?UTF-8?q?re=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/codebase-structure.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/contributing/codebase-structure.md diff --git a/docs/contributing/codebase-structure.md b/docs/contributing/codebase-structure.md new file mode 100644 index 0000000..e69de29 From f39bb4a6799733d7e553c757c2d6ca5d7ec3b736 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:26 +0000 Subject: [PATCH 292/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20dev=20setup=20inst?= =?UTF-8?q?ructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/dev-setup.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/contributing/dev-setup.md diff --git a/docs/contributing/dev-setup.md b/docs/contributing/dev-setup.md new file mode 100644 index 0000000..32e1eb8 --- /dev/null +++ b/docs/contributing/dev-setup.md @@ -0,0 +1,11 @@ +# Development Setup + +To contribute, clone the repo and run: + +```bash +./dev_setup.sh +``` + +You should not run unstrusted scripts so ask ChatGPT to explain what the contents of this script do! + +This will install and setup your development environment using [rye](https://rye-up.com) or pip. From 348f65cfb75c7a7ecbbeb6a05e736a0fe3dbd3e6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:05:26 +0000 Subject: [PATCH 293/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20initial=20TODOs=20?= =?UTF-8?q?markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/todo.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/contributing/todo.md diff --git a/docs/contributing/todo.md b/docs/contributing/todo.md new file mode 100644 index 0000000..9a8b483 --- /dev/null +++ b/docs/contributing/todo.md @@ -0,0 +1,9 @@ +# TODOs for writing the documentation + +- [ ] write out all .md files + +- [ ] call with julian + +- [ ] look into other repos for mkdocs inspiration + +- [ ] make a todo list for contributors From 30f9e5590e7c79bc276268ce330157d9ed2beb28 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:00 +0000 Subject: [PATCH 294/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20async.?= =?UTF-8?q?md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/async.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/concepts/async.md diff --git a/docs/concepts/async.md b/docs/concepts/async.md deleted file mode 100644 index e69de29..0000000 From a51b745150122c235b239c9b5be590815e4b4df5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 295/451] =?UTF-8?q?=E2=9C=A8=20Add=20chain=20documentation?= =?UTF-8?q?=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/chain.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/concepts/chain.md b/docs/concepts/chain.md index e69de29..3a7b7dd 100644 --- a/docs/concepts/chain.md +++ b/docs/concepts/chain.md @@ -0,0 +1,10 @@ +# chain() + +explain about chain like in usage.md and show achain and @runnable +exalain a bit how chain works under the hood + +## Usage + +## achain() + +## @runnable From 8170d097cd26923bc8b5fd90bd2ab56a8a064113 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 296/451] =?UTF-8?q?=F0=9F=93=9D=20Document=20error=20handl?= =?UTF-8?q?ing=20approach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/errors.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/concepts/errors.md b/docs/concepts/errors.md index e69de29..4dc82ab 100644 --- a/docs/concepts/errors.md +++ b/docs/concepts/errors.md @@ -0,0 +1,5 @@ +# Errors + +## Error Type + +(currently only supported for union output types (e.g. `Answer | Error`) so only openai models) From 3a67095af77f29ccc02319d1e3759f8db861dc0f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 297/451] =?UTF-8?q?=E2=9C=A8=20Add=20input=20concepts=20do?= =?UTF-8?q?cumentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/input.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/concepts/input.md b/docs/concepts/input.md index e69de29..9b55c58 100644 --- a/docs/concepts/input.md +++ b/docs/concepts/input.md @@ -0,0 +1,11 @@ +# Input Arguments + +## String Inputs + +## Pydantic Models + +## Other Types + +## Important Notes + +always use type hints From e4cd9f53932fd116081f0b6beddbee05de49105a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 298/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20LangChain=20docume?= =?UTF-8?q?ntation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/langchain.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/concepts/langchain.md diff --git a/docs/concepts/langchain.md b/docs/concepts/langchain.md new file mode 100644 index 0000000..54328d7 --- /dev/null +++ b/docs/concepts/langchain.md @@ -0,0 +1,11 @@ +# LangChain + +## What is LangChain? + +## Why building on top of it? + +## Compatibility + +## Runnables + +## Extension/Compatibility Examples From a0e37d27c8ab3649737aea27c24d48ceeda8b992 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 299/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20local=20models=20d?= =?UTF-8?q?ocumentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/local-models.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/concepts/local-models.md diff --git a/docs/concepts/local-models.md b/docs/concepts/local-models.md new file mode 100644 index 0000000..5dee660 --- /dev/null +++ b/docs/concepts/local-models.md @@ -0,0 +1,5 @@ +# Local Models + +## LlamaCPP + +## Grammars From 0f45066cb9a0d3ab4668deec2096b1ddec00292a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 300/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20models?= =?UTF-8?q?.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/models.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/concepts/models.md diff --git a/docs/concepts/models.md b/docs/concepts/models.md deleted file mode 100644 index e69de29..0000000 From 2e355469dd36e585466ad49915ae506e0c6c7907 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 301/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20parser=20documenta?= =?UTF-8?q?tion=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/parser.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/concepts/parser.md b/docs/concepts/parser.md index e69de29..f40e077 100644 --- a/docs/concepts/parser.md +++ b/docs/concepts/parser.md @@ -0,0 +1,13 @@ +# Output Parser + +## Output Type Hints + +## Strings + +The simplest output type is a string. The output parser will return the content of the AI response just as it is. + +## Pydantic Models + +## Primitive Types + +## Streaming From 367fab811c3371ee892c2266480da9ce5b957c4c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 302/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20prompt?= =?UTF-8?q?.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/prompt.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/concepts/prompt.md diff --git a/docs/concepts/prompt.md b/docs/concepts/prompt.md deleted file mode 100644 index e69de29..0000000 From 66363990149d1011d41e277893f80a9f8f3ebeae Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 303/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20prompting=20concep?= =?UTF-8?q?ts=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/prompting.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/concepts/prompting.md diff --git a/docs/concepts/prompting.md b/docs/concepts/prompting.md new file mode 100644 index 0000000..8cc0be7 --- /dev/null +++ b/docs/concepts/prompting.md @@ -0,0 +1,9 @@ +# Prompting + +## Jinja2 Templating + +## Input Argument Placement + +## ChatModel Behavior + +## Compilation Examples From 7b56394c8450d3ce669dab37c9f61aca16ae37c8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 304/451] =?UTF-8?q?=F0=9F=93=9A=20Add=20streaming=20docume?= =?UTF-8?q?ntation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/streaming.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/concepts/streaming.md diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md new file mode 100644 index 0000000..1866d7b --- /dev/null +++ b/docs/concepts/streaming.md @@ -0,0 +1,9 @@ +# Streaming + +## How to use streaming? + +## runnable streaming + +## Console Streaming + +## Streaming Examples \ No newline at end of file From 2a5d5c620c6e950d2894734682efbd68c1d9356a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 305/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20types=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/types.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/concepts/types.md diff --git a/docs/concepts/types.md b/docs/concepts/types.md deleted file mode 100644 index e69de29..0000000 From ecac6931ce0c516fb784033222ebd93fe0b320c0 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 306/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20unions.md=20docume?= =?UTF-8?q?ntation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/unions.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/concepts/unions.md diff --git a/docs/concepts/unions.md b/docs/concepts/unions.md new file mode 100644 index 0000000..26df726 --- /dev/null +++ b/docs/concepts/unions.md @@ -0,0 +1,7 @@ +# Union Types + +## Errors + +## Function Calling + +## Model Selection From 623114e60e85283e5e01aa986749be1ae4ddaf2f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:06:01 +0000 Subject: [PATCH 307/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20vision=20models=20?= =?UTF-8?q?documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/vision.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/concepts/vision.md diff --git a/docs/concepts/vision.md b/docs/concepts/vision.md new file mode 100644 index 0000000..cd78885 --- /dev/null +++ b/docs/concepts/vision.md @@ -0,0 +1,9 @@ +# Vision Models + +## GPT-4 Vision + +## Local Vision Models + +## Examples + +## Image Type From 656cdf0d5d7291ad918d6ae534813483673ef321 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:09:15 +0000 Subject: [PATCH 308/451] =?UTF-8?q?=F0=9F=94=A7=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/config.md | 101 +++++++++++++++++++++++++++++++++ docs/settings.md | 74 ------------------------ 2 files changed, 101 insertions(+), 74 deletions(-) create mode 100644 docs/getting-started/config.md delete mode 100644 docs/settings.md diff --git a/docs/getting-started/config.md b/docs/getting-started/config.md new file mode 100644 index 0000000..5015b4d --- /dev/null +++ b/docs/getting-started/config.md @@ -0,0 +1,101 @@ +# Funcchain Configuration + +## Set Global Settings + +In every project you use funcchain in you can specify global settings. This is done by importing the `settings` object from the `funcchain` package. + +```python +from funcchain import settings +``` + +You can then change the settings like here: + +```python +settings.llm = "openai/gpt-4-vision-preview" +``` + +## Set Local Settings + +If you want to set local settings only applied to a specific funcchain function you +can set them using the SettingsOverride class. + +```python +from funcchain import chain +from funcchain.settings import SettingsOverride + +def analyse_output( + goal: str + output: str, + settings: SettingsOverride = {}, +) -> OutputAnalysis: + """ + Analyse the output and determine if the goal is reached. + """ + return chain(settings_override=settings) +``` + +The `settings_override` argument is a `SettingsOverride` object which is a dict-like object that can be used to override the global settings. +You will get suggestions from your IDE on what settings you can override due to the type hints. + +## Settings Class Overview + +The configuration settings for Funcchain are encapsulated within the `FuncchainSettings` class. This class inherits from Pydantic's `BaseSettings`. + +`funcchain/backend/settings.py` + +```python +class FuncchainSettings(BaseSettings): + ... +``` + +## Setting Descriptions + +### General Settings + +- `debug: bool = True` + Enables or disables debug mode. + +- `llm: BaseChatModel | str = "openai/gpt-3.5-turbo-1106"` + Defines the language learning model to be used. It can be a type of `BaseChatModel` or `str` (model_name). + Checkout the [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) file for a list and schema of supported models. + +- `console_stream: bool = False` + Enables or disables token streaming to the console. + +- `system_prompt: str = ""` + System prompt used as first message in the chat to instruct the model. + +- `retry_parse: int = 3` + Number of retries for auto fixing pydantic validation errors. + +- `retry_parse_sleep: float = 0.1` + Sleep time between retries. + +### Model Keyword Arguments + +- `verbose: bool = False` + Enables or disables verbose logging for the model. + +- `streaming: bool = False` + Enables or disables streaming for the model. + +- `max_tokens: int = 2048` + Specifies the maximum number of output tokens for chat models. + +- `temperature: float = 0.1` + Controls the randomness in the model's output. + +### LlamaCPP Keyword Arguments + +- `context_lenght: int = 8196` + Specifies the context length for the LlamaCPP model. + +- `n_gpu_layers: int = 42` + Specifies the number of GPU layers for the LlamaCPP model. + Choose 0 for CPU only. + +- `keep_loaded: bool = False` + Determines whether to keep the LlamaCPP model loaded in memory. + +- `local_models_path: str = "./.models"` + Specifies the local path for storing models. diff --git a/docs/settings.md b/docs/settings.md deleted file mode 100644 index a9c26a9..0000000 --- a/docs/settings.md +++ /dev/null @@ -1,74 +0,0 @@ -# Settings - -## Settings Class Overview - -The configuration settings for Funcchain are encapsulated within the `FuncchainSettings` class. This class inherits from Pydantic's `BaseSettings`. - -`funcchain/config.py` - -```python -class FuncchainSettings(BaseSettings): - ... -``` - -## Setting Descriptions - -### General Settings - -- `llm: BaseChatModel | RunnableWithFallbacks | str = "openai/gpt-3.5-turbo"` - Defines the language learning model to be used. It can be a type of `BaseChatModel`, `RunnableWithFallbacks`, or `str` (model_name). - -- `verbose: bool = True` - Enables or disables verbose logging. - -### Prompt Settings - -- `max_tokens: int = 4096` - Specifies the maximum number of tokens for chat models. - -- `default_system_prompt: str = "You are a professional assistant solving tasks."` - Default prompt used for initializing the system. - -### API Keys - -- `openai_api_key: Optional[str] = None` - API key for the OpenAI service. - -- `azure_api_key: Optional[str] = None` - API key for the Azure service. - -- `anthropic_api_key: Optional[str] = None` - API key for the Anthropic service. - -- `google_api_key: Optional[str] = None` - API key for the Google service. - -- `JINACHAT_API_KEY: Optional[str] = None` - API key for the JinaChat service. - -### Azure Settings - -- `AZURE_API_BASE: Optional[str] = None` - Base URL for the Azure API. - -- `AZURE_DEPLOYMENT_NAME: str = "gpt-4"` - Deployment name for the Azure service. - -- `AZURE_DEPLOYMENT_NAME_LONG: Optional[str] = None` - Extended deployment name for the Azure service, if applicable. - -- `AZURE_API_VERSION: str = "2023-07-01-preview"` - API version for the Azure service. - -### Model Keyword Arguments - -- `temperature: float = 0.1` - Controls the randomness in the model's output. - -- `verbose: bool = False` - Enables or disables verbose logging for the model. - -### Additional Methods - -- `model_kwargs(self) -> dict[str, Any]` - Method that returns a dictionary of keyword arguments for the model initialization based on the settings. From 36a7a3e86e2b390aa9efb5ec2a0c7f163553fffa Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:09:52 +0000 Subject: [PATCH 309/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20models=20documenta?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/models.md | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/getting-started/models.md diff --git a/docs/getting-started/models.md b/docs/getting-started/models.md new file mode 100644 index 0000000..5958f32 --- /dev/null +++ b/docs/getting-started/models.md @@ -0,0 +1,43 @@ +# Supported Models + +## LangChain Chat Models + +You can set the `settings.llm` with any LangChain ChatModel. + +```python +from langchain_openai.chat_models import AzureChatOpenAI + +settings.llm = AzureChatOpenAI(...) +``` + +## String Model Identifiers + +You can also set the `settings.llm` with a string identifier of a ChatModel including local models. + +### Schema + +`/:` + +### Providers + +- `openai`: OpenAI Chat Models +- `llamacpp`: Run local models directly using llamacpp (alias: `thebloke`, `gguf`) +- `ollama`: Run local models through Ollama (wrapper for llamacpp) +- `azure`: Azure Chat Models +- `anthropic`: Anthropic Chat Models +- `google`: Google Chat Models + +### Examples + +- `openai/gpt-3.5-turbo`: ChatGPT Classic +- `openai/gpt-4-1106-preview`: GPT-4-Turbo +- `ollama/openchat`: OpenChat3.5-1210 +- `ollama/openhermes2.5-mistral`: OpenHermes 2.5 +- `llamacpp/openchat-3.5-1210`: OpenChat3.5-1210 +- `TheBloke/Nous-Hermes-2-SOLAR-10.7B-GGUF`: alias for `llamacpp/...` +- `TheBloke/openchat-3.5-0106-GGUF:Q3_K_L`: with Q label + +### additional notes + +Checkout the file `src/funcchain/model/defaults.py` for the code that parses the string identifier. +Feel free to create a PR to add more models to the defaults. Or tell me how wrong I am and create a better system. From 164bc0aeb1d234e5a0a70743714e8333093bdd0f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:09:52 +0000 Subject: [PATCH 310/451] =?UTF-8?q?=E2=9C=A8=20Add=20usage=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/usage.md | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/getting-started/usage.md b/docs/getting-started/usage.md index e69de29..ca67c01 100644 --- a/docs/getting-started/usage.md +++ b/docs/getting-started/usage.md @@ -0,0 +1,44 @@ +# Usage + +To write your cognitive architectures with the funcchain syntax you need to import the `chain` function from the `funcchain` package. + +```python +from funcchain import chain +``` + +This chain function it the core component of funcchain. +It takes the docstring, input arguments and return type of the function and compiles everything into a langchain prompt. +It then executes the prompt with your input arguments if you call the function and returns the parsed result. + +```python +def hello(lang1: str, lang2: str, lang3: str) -> list[str]: + """ + Say hello in these 3 languages. + """ + return chain() + +hello("German", "French", "Spanish") +``` + +The underlying chat in the background will look like this: + +```html + +LANG1: German +LANG2: French +LANG3: Spanish + +Say hello in these 3 languages. + + + +{ + "value": ["Hallo", "Bonjour", "Hola"] +} + +``` + +Funcchain is handling all the redundant and complicated structuring of your prompts so you can focus on the important parts of your code. + +All input arguments are automatically added to the prompt so the model has context about what you insert. +The return type is used to force the model using a json-schema to always return a json object in the desired shape. From 070f0af5434062eebe9eddf466d920c57e398102 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:11:13 +0000 Subject: [PATCH 311/451] =?UTF-8?q?=F0=9F=93=9D=20fix=20eof?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/streaming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index 1866d7b..39c0b6c 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -6,4 +6,4 @@ ## Console Streaming -## Streaming Examples \ No newline at end of file +## Streaming Examples From 208b0a9a8c6d7bddc52b915c23cf98d4fd69f639 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:18:30 +0000 Subject: [PATCH 312/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20exampl?= =?UTF-8?q?es.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/examples.md | 67 ------------------------------------------------ 1 file changed, 67 deletions(-) delete mode 100644 docs/examples.md diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index 6ee0a62..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,67 +0,0 @@ -# Examples - -## Basic Usage - -The `chain()` function allows you to call a prompt like a regular Python function. The docstring serves as the instructions and the return type annotation determines the output parsing. - -```python -from funcchain import chain - -def hello_world() -> str: - """ - Generate a friendly hello world message. - """ - return chain() - -print(hello_world()) -``` - -This will send the docstring to the AI assistant and parse the response as a string. - -## Pydantic Models - -You can use Pydantic models to validate the response. - -```python -from funcchain import chain -from pydantic import BaseModel - - -class Message(BaseModel): - text: str - - -def hello_message() -> Message: - """ - Generate a message object that says hello. - """ - return chain() - - -print(hello_message()) -``` - -Now the response will be parsed as a Message object. - -## Asynchronous Support - -Async functions are also supported with `achain()`: - -```python -import asyncio -from funcchain import achain - -async def async_hello() -> str: - """Say hello asynchronously""" - return await achain() - -print(asyncio.run(async_hello())) -``` - -This allows you to easily call AI functions from async code. - -The funcchain project makes it really simple to leverage large language models in your Python code! Check out the source code for more examples. - -## Advanced Examples - -For advanced examples, checkout the examples directory [here](https://github.com/shroominic/funcchain/tree/main/examples) From 32a08e254a89a238673ffaee9fc7a5a1f32aac96 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:18:30 +0000 Subject: [PATCH 313/451] =?UTF-8?q?=E2=9C=A8=20Add=20installation=20instru?= =?UTF-8?q?ctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/installation.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index e69de29..526d2eb 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -0,0 +1,27 @@ +# Installation + +```bash +pip install funcchain +``` + +For additional features you can also install: + +- funcchain (`langchain_core + openai`) +- funcchain[ollama] (you need to install this [ollama fork]() for grammar support) +- funcchain[llamacpp] (using `llama-cpp-python`) +- funcchain[pillow] (for vision model features) +- funcchain[all] (includes everything) + +To enter this in your terminal you need to write it like this: +`pip install "funcchain[all]"` + +## Environment + +Make sure to have an OpenAI API key in your environment variables. For example, + +```bash +export OPENAI_API_KEY=sk-********** +``` + +But you can also create a `.env` file in your current working directory and include the key there. +The dot env file will load automatically. From 9676d59d1e175e85282971413215a4ed701dd29f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 10:18:30 +0000 Subject: [PATCH 314/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20documentation?= =?UTF-8?q?=20and=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 69 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/docs/index.md b/docs/index.md index dd00a0a..a6187c8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,6 +3,7 @@ [![Version](https://badge.fury.io/py/funcchain.svg)](https://badge.fury.io/py/funcchain) [![code-check](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml) ![Downloads](https://img.shields.io/pypi/dm/funcchain) +[![Discord](https://img.shields.io/discord/1192334452110659664?label=discord)](https://discord.gg/TrwWWMXdtR) ![License](https://img.shields.io/pypi/l/funcchain) ![PyVersion](https://img.shields.io/pypi/pyversions/funcchain) @@ -39,28 +40,72 @@ export OPENAI_API_KEY=sk-********** from funcchain import chain def hello() -> str: - """Say hello in 3 languages""" + """ + Say hello in 3 languages. + """ return chain() -print(hello()) # -> Hello, Bonjour, Hola +print(hello()) # -> "Hallo, Bonjour, Hola" ``` This will call the OpenAI API and return the response. +Its using OpenAI since we did not specify a model and it will use the default model from the global settings of funcchain. -The `chain` function extracts the docstring as the prompt and the return type for parsing the response. +The underlying chat will look like this: -## Contributing +- User: "Say hello in 3 languages." +- AI: "Hallo, Bonjour, Hola" -To contribute, clone the repo and run: +The `chain()` function does all the magic in the background. It extracts the docstring, input arguments and return type of the function and compiles everything into a langchain prompt. -```bash -./dev_setup.sh -``` +## Complex Example -This will install pre-commit hooks, dependencies and set up the environment. +Here a more complex example of what is possible. We create nested pydantic models and use union types to let the model choose the best shape to parse your given list into. + +```python +from pydantic import BaseModel, Field +from funcchain import chain -To activate the virtual environment managed by poetry, you can use the following command: +# define nested models +class Item(BaseModel): + name: str = Field(description="Name of the item") + description: str = Field(description="Description of the item") + keywords: list[str] = Field(description="Keywords for the item") + +class ShoppingList(BaseModel): + items: list[Item] + store: str = Field(description="The store to buy the items from") + +class TodoList(BaseModel): + todos: list[Item] + urgency: int = Field(description="The urgency of all tasks (1-10)") + +# support for union types +def extract_list(user_input: str) -> TodoList | ShoppingList: + """ + The user input is either a shopping List or a todo list. + """ + return chain() -```bash -poetry shell +# the model will choose the output type automatically +lst = extract_list( + input("Enter your list: ") +) + +# custom handler based on type +match lst: + case ShoppingList(items=items, store=store): + print("Here is your Shopping List: ") + for item in items: + print(f"{item.name}: {item.description}") + print(f"You need to go to: {store}") + + case TodoList(todos=todos, urgency=urgency): + print("Here is your Todo List: ") + for item in todos: + print(f"{item.name}: {item.description}") + print(f"Urgency: {urgency}") ``` + +The pydantic models force the language model to output only in the specified format. The actual ouput is a json string which is parsed into the pydantic model. This allows for a seamless integration of the language model into your app. +The union type selection works by listing every pydantic model as seperate function call to the model. So the LLM will select the best fitting pydantic model based on the prompt and inputs. From 9e49c5d9ab13bce4117f82ad12318d654341f56f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 315/451] =?UTF-8?q?=F0=9F=94=84=20Update=20README=20format?= =?UTF-8?q?ting=20and=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 3956aa8..e14573f 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,15 @@ [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://docs.pydantic.dev/latest/contributing/#badges) [![Twitter Follow](https://img.shields.io/twitter/follow/shroominic?style=social)](https://x.com/shroominic) -
- ```bash - $ > pip install funcchain - ``` -
+```bash +pip install funcchain +``` + ## Introduction `funcchain` is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. -It works perfect with OpenAI Functions and soon with other models using JSONFormer. +It utilizes perfect with OpenAI Functions or LlamaCpp grammars (json-schema-mode) for efficient structured output. +In the backend it compiles the funcchain syntax into langchain runnables so you can easily invoke, stream or batch process your pipelines. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/ricklamers/funcchain-demo) @@ -154,32 +154,38 @@ print(poem.analysis) ## Features -- minimalistic and easy to use -- easy swap between openai and local models -- write prompts as python functions -- pydantic models for output schemas -- langchain core in the backend -- fstrings or jinja templates for prompts -- fully utilises OpenAI Functions or LlamaCpp Grammars +- pythonic +- easy swap between openai or local models +- dynamic output types (pydantic models, or primitives) +- vision llm support +- langchain_core as backend +- jinja templating for prompts +- reliable structured output +- auto retry parsing - langsmith support -- async and pythonic -- auto gguf model download from huggingface -- streaming support +- sync, async, streaming, parallel, fallbacks +- gguf download from huggingface +- type hints for all functions and mypy support +- chat router component +- composable with langchain LCEL +- easy error handling +- enums and literal support +- custom parsing types ## Documentation -Highly recommend to try out the examples in the `./examples` folder. +[Checkout the docs here](https://shroominic.github.io/funcchain/) 👈 -Coming soon... feel free to add helpful .md files :) +Also highly recommend to try and run the examples in the `./examples` folder. ## Contribution -You want to contribute? That's great! Please run the dev setup to get started: +You want to contribute? Thanks, that's great! +For more information checkout the [Contributing Guide](docs/contributing/dev-setup.md). +Please run the dev setup to get started: ```bash -> git clone https://github.com/shroominic/funcchain.git && cd funcchain +git clone https://github.com/shroominic/funcchain.git && cd funcchain -> ./dev_setup.sh +./dev_setup.sh ``` - -Thanks! From eb01d76f9d0b26033848e54d4ff20713d7c3195f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 316/451] =?UTF-8?q?=F0=9F=93=98=20Add=20custom=20parser=20?= =?UTF-8?q?documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/custom-parser-types.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/advanced/custom-parser-types.md diff --git a/docs/advanced/custom-parser-types.md b/docs/advanced/custom-parser-types.md new file mode 100644 index 0000000..df83448 --- /dev/null +++ b/docs/advanced/custom-parser-types.md @@ -0,0 +1,11 @@ +# Custom Parsers + +## Example + +## Grammars + +## Format Instructions + +## parse() Function + +## Write your own Parser From 13d5a0b6e087b072aedb73ae57667c2062352082 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 317/451] =?UTF-8?q?=E2=9C=A8=20Add=20customization=20docum?= =?UTF-8?q?entation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/customization.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/advanced/customization.md diff --git a/docs/advanced/customization.md b/docs/advanced/customization.md new file mode 100644 index 0000000..ad52637 --- /dev/null +++ b/docs/advanced/customization.md @@ -0,0 +1,9 @@ +# Customization + +## extra args inside chain + +## low level langchain + +## extra args inside @runnable + +## custom ll models From b28e49dbc35416348c6fac220a4772106df573fa Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 318/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20changelog=20str?= =?UTF-8?q?ucture.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/changelog.md diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..766195f --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,55 @@ +# Changelog + +## [0.2.0] - 2024-01-28 + +### Added + +- todo write about new features + +### Changed + +- todo write about changes + +### Deprecated + +- todo write about deprecations + +### Removed + +- todo write about removals + +### Fixed + +- todo write about fixes + +### Security + +- Updated dependencies to mitigate known vulnerabilities. + +## [0.1.10] - 2023-12-12 + +- universal model loader +- improved configuration +- SettingOverrides +- auto gguf model download +- optional dependencies +- improve examples +- other small improvements + +## [0.1.0] - [0.1.9] - 2023-12-01 + +- pydantic v2 +- llamacpp support +- auto retry parsing +- jinja templates +- multiple tiny improvements +- codebase refactor +- bug fixes + +## [0.0.1] - [0.1.7] - 2023-10-08 + +- Undocumented Experimental Releases + +## [0.0.1] - 2023-08-31 + +- Initial Release From 4cd960d5bc61fdb94e8a9f1f1c1165d7ab0d4a22 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 319/451] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20Add=20TODO=20for?= =?UTF-8?q?=20rewrite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/overview.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index 48b7a95..d4183c9 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -1,5 +1,7 @@ # Concepts +TODO: rewrite this + ## Concepts Overview | name | description | From 969401b5ab8710315aaa35c77f5cac1fffff84c8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 320/451] =?UTF-8?q?=F0=9F=93=9D=20Rename=20pydantic.md=20f?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/pydantic.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/concepts/pydantic.md diff --git a/docs/concepts/pydantic.md b/docs/concepts/pydantic.md new file mode 100644 index 0000000..e69de29 From 476798f671b904071391ceeebf04c35a88cc8c93 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 321/451] =?UTF-8?q?=F0=9F=93=9C=20Add=20Code=20of=20Conduc?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/code-of-conduct.md | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/contributing/code-of-conduct.md diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md new file mode 100644 index 0000000..254c4e4 --- /dev/null +++ b/docs/contributing/code-of-conduct.md @@ -0,0 +1,43 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html From fbfeda9b849d6da6781a43ffaf6db7f344fe4f1e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 322/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20contributors=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/contributors.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/contributing/contributors.md diff --git a/docs/contributing/contributors.md b/docs/contributing/contributors.md new file mode 100644 index 0000000..c4c1b31 --- /dev/null +++ b/docs/contributing/contributors.md @@ -0,0 +1,15 @@ +# Contributors + +We would like to acknowledge the contributions of the following people: + + Name | Contribution | +------|--------------| + | | + +## How to Contribute + +If you would like to contribute to this project, please follow the guidelines in our [Contributing Guide](dev-setup.md). + +## Code of Conduct + +Please note that this project is released with a [Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms. From 907e06362ce55f7c4c05f0a8993582991c352a55 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 323/451] =?UTF-8?q?=F0=9F=93=84=20Add=20MIT=20License=20do?= =?UTF-8?q?c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/license.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/contributing/license.md diff --git a/docs/contributing/license.md b/docs/contributing/license.md new file mode 100644 index 0000000..19c0f00 --- /dev/null +++ b/docs/contributing/license.md @@ -0,0 +1,6 @@ +# License + +## MIT License + +All contributions are made under the MIT License. +See LICENSE.md for more information. From 8e43d11cab1beaf0efb2cd39205b452ce7272980 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 324/451] =?UTF-8?q?=F0=9F=93=9D=20Initialize=20roadmap=20d?= =?UTF-8?q?ocumentation=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/roadmap.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/contributing/roadmap.md diff --git a/docs/contributing/roadmap.md b/docs/contributing/roadmap.md new file mode 100644 index 0000000..40f2074 --- /dev/null +++ b/docs/contributing/roadmap.md @@ -0,0 +1,9 @@ +# TODOs for writing the documentation + +- [ ] write out all scratched docs .md files + +- [ ] look into other repos for mkdocs inspiration + +- [ ] make this file a todo list for contributors + +- [ ] add api reference? \ No newline at end of file From c69bedb8ef347bfbecee43ecda67c968a37cc9e4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 325/451] =?UTF-8?q?=F0=9F=94=92=20Add=20security=20guideli?= =?UTF-8?q?nes=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/security.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/contributing/security.md diff --git a/docs/contributing/security.md b/docs/contributing/security.md new file mode 100644 index 0000000..e69de29 From 075a5bf74f31b6ea4cd31e4260b91f5d6f04ed71 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 326/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20todo.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/todo.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 docs/contributing/todo.md diff --git a/docs/contributing/todo.md b/docs/contributing/todo.md deleted file mode 100644 index 9a8b483..0000000 --- a/docs/contributing/todo.md +++ /dev/null @@ -1,9 +0,0 @@ -# TODOs for writing the documentation - -- [ ] write out all .md files - -- [ ] call with julian - -- [ ] look into other repos for mkdocs inspiration - -- [ ] make a todo list for contributors From c339130c495db0d91c0472cbd6e42847cf360d8a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 327/451] =?UTF-8?q?=F0=9F=93=9D=20Renamed=20chat=20documen?= =?UTF-8?q?tation=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/chat.md | 126 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/features/chat.md diff --git a/docs/features/chat.md b/docs/features/chat.md new file mode 100644 index 0000000..9a37200 --- /dev/null +++ b/docs/features/chat.md @@ -0,0 +1,126 @@ +## Simple chatgpt rebuild with memory/history. +!!! Example + chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) + +!!! Important + Ensure you have set up your API key for the LLM of your choice, or Funcchain will look for a `.env` file. So in `.env` set up your key. + ```python + OPENAI_API_KEY="sk-XXX" + ``` + + +## Code Example + +```python +from funcchain import chain, settings +from funcchain.utils.memory import ChatMessageHistory + +settings.llm = "openai/gpt-4" +settings.console_stream = True + +history = ChatMessageHistory() + + +def ask(question: str) -> str: + return chain( + system="You are an advanced AI Assistant.", + instruction=question, + memory=history, + ) + + +def chat_loop() -> None: + while True: + query = input("> ") + + if query == "exit": + break + + if query == "clear": + global history + history.clear() + print("\033c") + continue + + ask(query) + + +if __name__ == "__main__": + print("Hey! How can I help you?\n") + chat_loop() +``` + + + +
+ ```terminal + initial print function: + $ Hey! How can I help you? + $ > + + userprompt: + $ > Say that Funcchain is cool + + assistant terminal asnwer: + $ Funcchain is cool. + ``` +
+ +## Instructions + +!!! Step-by-Step + **Import nececary funcchain components** + + ```python + from funcchain import chain, settings + from funcchain.utils.memory import ChatMessageHistory + ``` + + **Settings** + + ```python + settings.llm = "openai/gpt-4" + settings.console_stream = True + ``` + + + Funcchain supports multiple LLMs and has the ability to stream received LLM text instead of waiting for the complete answer. For configuration options, see below: + + ```markdown + - `settings.llm`: Specify the language model to use. See MODELS.md for available options. + - Streaming: Set `settings.console_stream` to `True` to enable streaming, + or `False` to disable it. + ``` + + [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) + + + **Establish a chat history** + + ```python + history = ChatMessageHistory() + ``` + Stores messages in an in memory list. This will crate a thread of messages. + + See [memory.py] //Todo: Insert Link + + + **Define ask function** + See how funcchain uses `chain()` with an input `str` to return an output of type `str` + + ```python + def ask(question: str) -> str: + return chain( + system="You are an advanced AI Assistant.", + instruction=question, + memory=history, + ) + ``` + + This function sends a question to the Funcchain `chain()` function. + + It sets the system context as an advanced AI Assistant and passes the question as an instruction. + + The history object is used to maintain a thread of messages for context. + + The function returns the response from the chain function. From 623fd274797493443f8330d988ab15ed65e2130c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 328/451] =?UTF-8?q?=F0=9F=94=80=20Rename=20dynamic=5Froute?= =?UTF-8?q?r=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/dynamic_router.md | 239 ++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 docs/features/dynamic_router.md diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md new file mode 100644 index 0000000..696190e --- /dev/null +++ b/docs/features/dynamic_router.md @@ -0,0 +1,239 @@ +# Dynamic Chat Router with Funcchain + +!!! Example + dynamic_router.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/dynamic_router.py) + +In this example we will use funcchain to build a LLM routing pipeline. +This is a very useful LLM task and can be used in a variety of applications. +You can abstract this for your own usage. +This should serve as an example of how to archive complex structures using funcchain. + +A dynamic chat router that selects the appropriate handler for user queries based on predefined routes. + +## Full Code Example + +```python +from enum import Enum +from typing import Any, Callable, TypedDict + +from funcchain.syntax.executable import compile_runnable +from pydantic import BaseModel, Field + +# Dynamic Router Definition: + + +class Route(TypedDict): + handler: Callable + description: str + + +class DynamicChatRouter(BaseModel): + routes: dict[str, Route] + + def _routes_repr(self) -> str: + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) + + def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) + + class RouterModel(BaseModel): + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) + + route_query = compile_runnable( + instruction="Given the user query select the best query handler for it.", + input_args=["user_query", "query_handlers"], + output_type=RouterModel, + ) + + selected_route = route_query.invoke( + input={ + "user_query": user_query, + "query_handlers": self._routes_repr(), + } + ).selector + assert isinstance(selected_route, str) + + return self.routes[selected_route]["handler"](user_query, **kwargs) + + +# Example Usage: + + +def handle_pdf_requests(user_query: str) -> str: + return "Handling PDF requests with user query: " + user_query + + +def handle_csv_requests(user_query: str) -> str: + return "Handling CSV requests with user query: " + user_query + + +def handle_default_requests(user_query: str) -> str: + return "Handling DEFAULT requests with user query: " + user_query + + +router = DynamicChatRouter( + routes={ + "pdf": { + "handler": handle_pdf_requests, + "description": "Call this for requests including PDF Files.", + }, + "csv": { + "handler": handle_csv_requests, + "description": "Call this for requests including CSV Files.", + }, + "default": { + "handler": handle_default_requests, + "description": "Call this for all other requests.", + }, + }, +) + + +router.invoke_route("Can you summarize this csv?") +``` + +Demo +
+```python +User: +$ Can you summarize this csv? +$ ............... +Handling CSV requests with user query: Can you summarize this csv? +``` +
+ +## Instructions + +!!! Step-by-Step + + **Nececary imports** + ``` + from enum import Enum + from typing import Any, Callable, TypedDict + + from funcchain.syntax.executable import compile_runnable + from pydantic import BaseModel, Field + ``` + + **Define Route Type** + ```python + class Route(TypedDict): + handler: Callable + description: str + ``` + + Create a `TypedDict` to define the structure of a route with a handler function and a description. Just leave this unchanged if not intentionally experimenting. + + **Implement Route Representation** + Establish a Router class + ```python + class DynamicChatRouter(BaseModel): + routes: dict[str, Route] + ``` + + **_routes_repr():** + Returns a string representation of all routes and their descriptions, used to help the language model understand the available routes. + + ```python + def _routes_repr(self) -> str: + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) + ``` + + **invoke_route(user_query: str, **kwargs: Any) -> Any: ** + This method takes a user query and additional keyword arguments. Inside invoke_route, an Enum named RouteChoices is dynamically created with keys corresponding to the route names. This Enum is used to validate the selected route. + ```python + def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) + ``` + + **Compile the Route Selection Logic** + The `RouterModel` class in this example is used for defining the expected output structure that the `compile_runnable` function will use to determine the best route for a given user query. + + + ```python + class RouterModel(BaseModel): + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) + + route_query = compile_runnable( + instruction="Given the user query select the best query handler for it.", + input_args=["user_query", "query_handlers"], + output_type=RouterModel, + ) + + selected_route = route_query.invoke( + input={ + "user_query": user_query, + "query_handlers": self._routes_repr(), + } + ).selector + assert isinstance(selected_route, str) + + return self.routes[selected_route]["handler"](user_query, **kwargs) + ``` + + - `RouterModel`: Holds the route selection with a default option, ready for you to play around with. + - `RouteChoices`: An Enum built from route names, ensuring you only get valid route selections. + - `compile_runnable`: Sets up the decision-making logic for route selection, guided by the provided instruction and inputs. + - `route_query`: Calls the decision logic with the user's query and a string of route descriptions. + - `selected_route`: The outcome of the decision logic, representing the route to take. + - `assert`: A safety check to confirm the route is a string, as expected by the routes dictionary. + - `handler invocation`: Runs the chosen route's handler with the provided query and additional arguments. + + **Define route functions** + + Now you can use the structured output to execute programatically based on a natural language input. + Establish functions tailored to your needs. + ```python + def handle_pdf_requests(user_query: str) -> str: + return "Handling PDF requests with user query: " + user_query + + + def handle_csv_requests(user_query: str) -> str: + return "Handling CSV requests with user query: " + user_query + + + def handle_default_requests(user_query: str) -> str: + return "Handling DEFAULT requests with user query: " + user_query + ``` + **Define the routes** + And bind the previous established functions. + + ```python + router = DynamicChatRouter( + routes={ + "pdf": { + "handler": handle_pdf_requests, + "description": "Call this for requests including PDF Files.", + }, + "csv": { + "handler": handle_csv_requests, + "description": "Call this for requests including CSV Files.", + }, + "default": { + "handler": handle_default_requests, + "description": "Call this for all other requests.", + }, + }, + ) + ``` + + **Get output** + Use the router.invoke_route method to process the user query and obtain the appropriate response. + + ```python + router.invoke_route("Can you summarize this csv?") + ``` \ No newline at end of file From a6e9b29fe9d45fe3c7387b3af7e432bbdf080a5f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 329/451] =?UTF-8?q?=F0=9F=93=9D=20Rename=20enums=20documen?= =?UTF-8?q?tation=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/enums.md | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/features/enums.md diff --git a/docs/features/enums.md b/docs/features/enums.md new file mode 100644 index 0000000..d8b0071 --- /dev/null +++ b/docs/features/enums.md @@ -0,0 +1,92 @@ +##Decision Making with Enums and Funcchain + +!!! Example + See [enums.py](https://github.com/shroominic/funcchain/blob/main/examples/enums.py) + + In this example, we will use the enum module and funcchain library to build a decision-making system. + This is a useful task for creating applications that require predefined choices or responses. + You can adapt this for your own usage. + This serves as an example of how to implement decision-making logic using enums and the funcchain library. + +##Full Code Example +A simple system that takes a question and decides a 'yes' or 'no' answer based on the input. + +```python +from enum import Enum +from funcchain import chain +from pydantic import BaseModel + +class Answer(str, Enum): + yes = "yes" + no = "no" + +class Decision(BaseModel): + answer: Answer + +def make_decision(question: str) -> Decision: + """ + Based on the question decide yes or no. + """ + return chain() + +if __name__ == "__main__": + print(make_decision("Do you like apples?")) +``` + +#Demo +
+ ```terminal + User: + $ Are apples red? + $ ............... + Decision(answer=) + ``` +
+ +##Instructions + +!!! Step-by-Step + **Necessary Imports** + ```python + from enum import Enum + from funcchain import chain + from pydantic import BaseModel + ``` + + **Define the Answer Enum** + The Answer enum defines possible answers as 'yes' and 'no', which are the only valid responses for the decision-making system. Experiment by using and describing other enums. + + ```python + class Answer(str, Enum): + yes = "yes" + no = "no" + ``` + **Create the Decision Model** + The Decision class uses Pydantic to model a decision, ensuring that the answer is always an instance of the Answer enum. + + ```python + class Decision(BaseModel): + answer: Answer + ``` + + **Implement the Decision Function** + The make_decision function is where the decision logic will be implemented, using `chain()` to process the question and return a decision. + When using your own enums you want to edit this accordingly. + + ```python + def make_decision(question: str) -> Decision: + """ + Based on the question decide yes or no. + """ + return chain() + ``` + + **Run the Decision System** + This block runs the decision-making system, printing out the decision for a given question when the script is executed directly. + + + ```python + if __name__ == "__main__": + print(make_decision("Do you like apples?")) + + ``` \ No newline at end of file From 08350e1b86d4563ac3591d493c7afc37d4761356 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 330/451] =?UTF-8?q?=F0=9F=93=9D=20Rename=20error=5Foutput?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/error_output.md | 91 +++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/features/error_output.md diff --git a/docs/features/error_output.md b/docs/features/error_output.md new file mode 100644 index 0000000..85629b4 --- /dev/null +++ b/docs/features/error_output.md @@ -0,0 +1,91 @@ +#Example of raising an error + +!!! Example + error_output.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/error_output.py) + + In this example, we will use the funcchain library to build a system that extracts user information from text. + Most importantly we will be able to raise an error thats programmatically usable. + You can adapt this for your own usage. + + + The main functionality is to take a string of text and attempt to extract user information, such as name and email, and return a User object. If the information is insufficient, an Error is returned instead. + +##Full Code Example + +```python +from funcchain import BaseModel, Error, chain +from rich import print + +class User(BaseModel): + name: str + email: str | None + +def extract_user_info(text: str) -> User | Error: + """ + Extract the user information from the given text. + In case you do not have enough infos, raise. + """ + return chain() + +if __name__ == "__main__": + print(extract_user_info("hey")) # returns Error + print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object + +``` + +Demo +
+ ```python + $ print(extract_user_info("hey")) + + Error: Insufficient information to extract user details. + + User: + $ print(extract_user_info("I'm John and my mail is john@gmail.com")) + + I'm John and my mail is john@gmail.com + User(name='John', email='john@gmail.com') + + //update example + ``` +
+ +##Instructions + +!!! Step-by-Step + + **Necessary Imports** + ```python + from funcchain import BaseModel, Error, chain + from rich import print + ``` + + **Define the User Model** + ```python + class User(BaseModel): + name: str + email: str | None + ``` + The User class is a Pydantic model that defines the structure of the user information to be extracted, with fields for `name` and an email. + Change the fields to experiment and alignment with your project. + + **Implement the Extraction Function** + The `extract_user_info` function is intended to process the input text and return either a User object with extracted information or an Error if the information is not sufficient. + ```python + def extract_user_info(text: str) -> User | Error: + """ + Extract the user information from the given text. + In case you do not have enough infos, raise. + """ + return chain() + ``` + For experiments and adoptions also change the `str` that will be used in chain() to identify what you defined earlier in the `User(BaseModel)` + + + **Run the Extraction System** + This conditional block is used to execute the extraction function and print the results when the script is run directly. + ```python + if __name__ == "__main__": + print(extract_user_info("hey")) # returns Error + print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object + ``` \ No newline at end of file From 54dd0b7483d84ecb07c61fb41560a1f0284b3015 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 331/451] =?UTF-8?q?=F0=9F=93=9D=20Rename=20literals=20docu?= =?UTF-8?q?mentation=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/literals.md | 88 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/features/literals.md diff --git a/docs/features/literals.md b/docs/features/literals.md new file mode 100644 index 0000000..4cf668f --- /dev/null +++ b/docs/features/literals.md @@ -0,0 +1,88 @@ +#Literal Type Enforcement in Funcchain + +!!! Example + literals.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/literals.py) + + This is a useful task for scenarios where you want to ensure that certain outputs strictly conform to a predefined set of values. + This serves as an example of how to implement strict type checks on outputs using the Literal type from the typing module and the funcchain library. + + You can adapt this for your own usage. + +##Full Code Example + +```python +from typing import Literal +from funcchain import chain +from pydantic import BaseModel + +class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] + +def rank_output(output: str) -> Ranking: + """ + Analyze and rank the output. + """ + return chain() + +if __name__ == "__main__": + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) +``` + +Demo +
+```python +$ rank = rank_output("The quick brown fox jumps over the lazy dog.") +$ ........ +Ranking(chain_of_thought='...', score=33, error='all_good') +``` +
+ +##Instructions + +!!! Step-by-Step + + **Necessary Imports** + ```python + from typing import Literal + from funcchain import chain + from pydantic import BaseModel + ``` + + + **Define the Ranking Model** + The Ranking class is a Pydantic model that uses the Literal type to ensure that the score and error fields can only contain certain predefined values. + So experiment with changing those but keeping this structure of the class. + The LLM will be forced to deliver one of the defined output. + + ```python + class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] + ``` + + **Implement the Ranking Function** + Use `chain()` to process a user input, which must be a string. + Adjust the content based on your above defined class. + + ```python + def rank_output(output: str) -> Ranking: + """ + Analyze and rank the output. + """ + return chain() + ``` + + **Execute the Ranking System** + This block is used to execute the ranking function and print the results when the script is run directly. + ```python + if __name__ == "__main__": + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) + ``` + + + From 00c0856ca7103c254562ab27d0784d1a8709363e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 332/451] =?UTF-8?q?=E2=9C=A8=20Add=20settings=5Foverride?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/config.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/getting-started/config.md b/docs/getting-started/config.md index 5015b4d..7e55aa6 100644 --- a/docs/getting-started/config.md +++ b/docs/getting-started/config.md @@ -32,6 +32,12 @@ def analyse_output( Analyse the output and determine if the goal is reached. """ return chain(settings_override=settings) + +result = analyse_output( + "healthy outpout", + "Hello World!", + settings_override={"llm": "openai/gpt-4-vision-preview"}, +) ``` The `settings_override` argument is a `SettingsOverride` object which is a dict-like object that can be used to override the global settings. From eb3db01a1eada6868b964b06acec16f1340ef7d5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 333/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20installation=20?= =?UTF-8?q?instructions=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/installation.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 7df28a3..5f17f2b 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,18 +1,21 @@ # Installation
+ ```bash -pip install funcchain +$ pip install funcchain +---> 100% ``` +
For additional features you can also install: -- funcchain (`langchain_core + openai`) -- funcchain[ollama] (you need to install this [ollama fork]() for grammar support) -- funcchain[llamacpp] (using `llama-cpp-python`) -- funcchain[pillow] (for vision model features) -- funcchain[all] (includes everything) +- `funcchain` (langchain_core + openai) +- `funcchain[ollama]` (you need to install this [ollama fork](https://github.com/ollama/ollama/pull/1606) for grammar support) +- `funcchain[llamacpp]` (using llama-cpp-python) +- `funcchain[pillow]` (for vision model features) +- `funcchain[all]` (includes everything) To enter this in your terminal you need to write it like this: `pip install "funcchain[all]"` @@ -21,9 +24,13 @@ To enter this in your terminal you need to write it like this: Make sure to have an OpenAI API key in your environment variables. For example, +
+ ```bash -export OPENAI_API_KEY=sk-********** +$ export OPENAI_API_KEY="sk-rnUPxirSQ4bmz2He4qyaiKShdXJcsOsTg" ``` +
+ But you can also create a `.env` file in your current working directory and include the key there. The dot env file will load automatically. From d6e7c304e3a0350d4ca8fdafc82a8ecca6374c07 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 334/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20installation=20?= =?UTF-8?q?command,=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/introduction.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index 8c40f55..b6ca4f2 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -6,9 +6,11 @@ [![Twitter Follow](https://img.shields.io/twitter/follow/shroominic?style=social)](https://x.com/shroominic)
+ ```bash - $ > pip install funcchain + $ pip install funcchain ``` +
!!! Important @@ -305,7 +307,7 @@ for obj in result.objects: ``` **Define Model** - set global llm using model identifiers see [MODELS.md]((https://github.com/shroominic/funcchain/blob/main/MODELS.md)) + set global llm using model identifiers see [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) ```python settings.llm = "openai/gpt-4-vision-preview" ``` From 0108e0fe296240cbaf7dae460329c9eed670fe82 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 335/451] =?UTF-8?q?=E2=9C=A8=20Add=20AzureChatOpenAI=20exa?= =?UTF-8?q?mple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/models.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/getting-started/models.md b/docs/getting-started/models.md index 5958f32..f9c21a4 100644 --- a/docs/getting-started/models.md +++ b/docs/getting-started/models.md @@ -5,6 +5,7 @@ You can set the `settings.llm` with any LangChain ChatModel. ```python +from funcchain import settings from langchain_openai.chat_models import AzureChatOpenAI settings.llm = AzureChatOpenAI(...) @@ -14,6 +15,14 @@ settings.llm = AzureChatOpenAI(...) You can also set the `settings.llm` with a string identifier of a ChatModel including local models. +```python +from funcchain import settings + +settings.llm = "llamacpp/openchat-3.5-1210" + +# ... +``` + ### Schema `/:` From a5dbdb793356bd6eef165e1ecf20d2f059104336 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 336/451] =?UTF-8?q?=F0=9F=93=A6=20Update=20installation=20?= =?UTF-8?q?instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 236aa52..c36c05b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,17 +26,20 @@ Key features: ## Installation
+ ```bash -# pip install funcchain +$ pip install funcchain +---> 100% ``` +
!!! Important Make sure to have an OpenAI API key in your environment variables. For example, - ```bash - export OPENAI_API_KEY=sk-********** - ``` +```bash +export OPENAI_API_KEY=sk-********** +``` ## Usage From 02796c169f5292b37dbd2f7c07b8c29578de0585 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 337/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20smart?= =?UTF-8?q?=5Fquestion.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/todo/smart_question.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 examples/todo/smart_question.py diff --git a/examples/todo/smart_question.py b/examples/todo/smart_question.py deleted file mode 100644 index 1070f07..0000000 --- a/examples/todo/smart_question.py +++ /dev/null @@ -1,16 +0,0 @@ -from funcchain import Matrix, achain # type: ignore - -# Matrix is a type annotation that tells the backend -# to run n versions of this prompt in parallel and -# summarizes the results. -# This corrects for any errors in the model and improves -# the quality of the answer. - - -# NOT YET WORKING (TODO) -async def generate_answer(question: Matrix[str], context: list[str] = []) -> str: - """ - Generate an answer to the question based on the context. - If no context is provided just use the question. - """ - return await achain() From c1b26216cd89046561524e05778fb6ca33d02f1c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:46 +0000 Subject: [PATCH 338/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20documentation?= =?UTF-8?q?=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 55 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3cb00dd..e79b936 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,27 +5,50 @@ repo_name: shroominic/funcchain repo_url: https://github.com/shroominic/funcchain/ nav: - - 'Introduction': - - 'Funcchain': 'getting-started/introduction.md' + - 'Funcchain': 'index.md' - 'Getting Started': - - 'Welcome': 'index.md' + - 'Introduction': 'getting-started/introduction.md' - 'Installation': 'getting-started/installation.md' - 'Usage': 'getting-started/usage.md' + - 'Configuration': 'getting-started/config.md' + - 'Models': 'getting-started/models.md' - 'Concepts': - 'Overview': 'concepts/overview.md' - - 'Chain': 'chain.md' - - 'Input Args': 'input.md' - - 'Prompt Template': 'prompt.md' - - 'Output Parser': 'parser.md' - - 'Pydantic Models': 'models.md' - - 'Settings': 'settings.md' - - 'Examples': - - 'ChatGPT': 'examples/chat.md' - - 'Dynamic Router': 'examples/dynamic_router.md' - - 'Enums': 'examples/enums.md' - - 'Error Output': 'examples/error_output.md' - - 'Literals': 'examples/literals.md' - - 'Union Types': 'examples/union.md' + - 'Chain': 'concepts/chain.md' + - 'Input Args': 'concepts/input.md' + - 'Prompting': 'concepts/prompting.md' + - 'Output Parsing': 'concepts/parser.md' + - 'Pydantic Models': 'concepts/pydantic.md' + - 'Errors': 'concepts/errors.md' + - 'Langchain': 'concepts/langchain.md' + - 'Local Models': 'concepts/local-models.md' + - 'Pydantic': 'concepts/pydantic.md' + - 'Streaming': 'concepts/streaming.md' + - 'Unions': 'concepts/unions.md' + - 'Vision': 'concepts/vision.md' + - 'Features': + - 'ChatGPT': 'features/chat.md' + - 'Dynamic Router': 'features/dynamic_router.md' + - 'Enums': 'features/enums.md' + - 'Error Output': 'features/error_output.md' + - 'Literals': 'features/literals.md' + - 'Advanced': + - 'Async': 'advanced/async.md' + - 'Signature': 'advanced/signature.md' + - 'Runnables': 'advanced/runnables.md' + - 'Codebase Scaling': 'advanced/codebase-scaling.md' + - 'Customization': 'advanced/customization.md' + - 'Stream Parsing': 'advanced/stream-parsing.md' + - 'Contributing': + - 'Contributing': 'contributing/dev-setup.md' + - 'Codebase Structure': 'contributing/codebase-structure.md' + - 'Code of Conduct': 'contributing/code-of-conduct.md' + - 'Contributors': 'contributing/contributors.md' + - 'Security': 'contributing/security.md' + - 'Roadmap': 'contributing/roadmap.md' + - 'License': 'contributing/license.md' + - 'Changelog': 'changelog.md' + - 'API Reference': 'api.md' theme: name: material From f4797d2d482f3748f6f5694f9ac311c6ef83502f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:05:47 +0000 Subject: [PATCH 339/451] =?UTF-8?q?=F0=9F=94=80=20Renamed=20primitive=5Fty?= =?UTF-8?q?pe.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/primitive_type.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/funcchain/parser/primitive_type.py diff --git a/src/funcchain/parser/primitive_type.py b/src/funcchain/parser/primitive_type.py new file mode 100644 index 0000000..68bcf3f --- /dev/null +++ b/src/funcchain/parser/primitive_type.py @@ -0,0 +1,20 @@ +from typing import TypeVar + +from langchain_core.output_parsers import BaseOutputParser + +from ..syntax.output_types import CodeBlock as CodeBlock + +T = TypeVar("T") + + +# TODO: remove and implement primitive type output parser using nested pydantic extraction +class BoolOutputParser(BaseOutputParser[bool]): + def parse(self, text: str) -> bool: + return text.strip()[:1].lower() == "y" + + def get_format_instructions(self) -> str: + return "\nAnswer only with 'Yes' or 'No'." + + @property + def _type(self) -> str: + return "bool" From c0a40994f4bc8d924e7bc7b68a7dbb5667183c88 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 340/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20chat.m?= =?UTF-8?q?d=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/examples/chat.md | 126 ------------------------------------------ 1 file changed, 126 deletions(-) delete mode 100644 docs/examples/chat.md diff --git a/docs/examples/chat.md b/docs/examples/chat.md deleted file mode 100644 index 9a37200..0000000 --- a/docs/examples/chat.md +++ /dev/null @@ -1,126 +0,0 @@ -## Simple chatgpt rebuild with memory/history. -!!! Example - chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) - -!!! Important - Ensure you have set up your API key for the LLM of your choice, or Funcchain will look for a `.env` file. So in `.env` set up your key. - ```python - OPENAI_API_KEY="sk-XXX" - ``` - - -## Code Example - -```python -from funcchain import chain, settings -from funcchain.utils.memory import ChatMessageHistory - -settings.llm = "openai/gpt-4" -settings.console_stream = True - -history = ChatMessageHistory() - - -def ask(question: str) -> str: - return chain( - system="You are an advanced AI Assistant.", - instruction=question, - memory=history, - ) - - -def chat_loop() -> None: - while True: - query = input("> ") - - if query == "exit": - break - - if query == "clear": - global history - history.clear() - print("\033c") - continue - - ask(query) - - -if __name__ == "__main__": - print("Hey! How can I help you?\n") - chat_loop() -``` - - - -
- ```terminal - initial print function: - $ Hey! How can I help you? - $ > - - userprompt: - $ > Say that Funcchain is cool - - assistant terminal asnwer: - $ Funcchain is cool. - ``` -
- -## Instructions - -!!! Step-by-Step - **Import nececary funcchain components** - - ```python - from funcchain import chain, settings - from funcchain.utils.memory import ChatMessageHistory - ``` - - **Settings** - - ```python - settings.llm = "openai/gpt-4" - settings.console_stream = True - ``` - - - Funcchain supports multiple LLMs and has the ability to stream received LLM text instead of waiting for the complete answer. For configuration options, see below: - - ```markdown - - `settings.llm`: Specify the language model to use. See MODELS.md for available options. - - Streaming: Set `settings.console_stream` to `True` to enable streaming, - or `False` to disable it. - ``` - - [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) - - - **Establish a chat history** - - ```python - history = ChatMessageHistory() - ``` - Stores messages in an in memory list. This will crate a thread of messages. - - See [memory.py] //Todo: Insert Link - - - **Define ask function** - See how funcchain uses `chain()` with an input `str` to return an output of type `str` - - ```python - def ask(question: str) -> str: - return chain( - system="You are an advanced AI Assistant.", - instruction=question, - memory=history, - ) - ``` - - This function sends a question to the Funcchain `chain()` function. - - It sets the system context as an advanced AI Assistant and passes the question as an instruction. - - The history object is used to maintain a thread of messages for context. - - The function returns the response from the chain function. From a547e1ef33fdb9fd8bd539bdd5d3460b28c1ca5f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 341/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20dynami?= =?UTF-8?q?c=5Frouter.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/examples/dynamic_router.md | 239 -------------------------------- 1 file changed, 239 deletions(-) delete mode 100644 docs/examples/dynamic_router.md diff --git a/docs/examples/dynamic_router.md b/docs/examples/dynamic_router.md deleted file mode 100644 index 696190e..0000000 --- a/docs/examples/dynamic_router.md +++ /dev/null @@ -1,239 +0,0 @@ -# Dynamic Chat Router with Funcchain - -!!! Example - dynamic_router.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/dynamic_router.py) - -In this example we will use funcchain to build a LLM routing pipeline. -This is a very useful LLM task and can be used in a variety of applications. -You can abstract this for your own usage. -This should serve as an example of how to archive complex structures using funcchain. - -A dynamic chat router that selects the appropriate handler for user queries based on predefined routes. - -## Full Code Example - -```python -from enum import Enum -from typing import Any, Callable, TypedDict - -from funcchain.syntax.executable import compile_runnable -from pydantic import BaseModel, Field - -# Dynamic Router Definition: - - -class Route(TypedDict): - handler: Callable - description: str - - -class DynamicChatRouter(BaseModel): - routes: dict[str, Route] - - def _routes_repr(self) -> str: - return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) - - def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: - RouteChoices = Enum( # type: ignore - "RouteChoices", - {r: r for r in self.routes.keys()}, - type=str, - ) - - class RouterModel(BaseModel): - selector: RouteChoices = Field( - default="default", - description="Enum of the available routes.", - ) - - route_query = compile_runnable( - instruction="Given the user query select the best query handler for it.", - input_args=["user_query", "query_handlers"], - output_type=RouterModel, - ) - - selected_route = route_query.invoke( - input={ - "user_query": user_query, - "query_handlers": self._routes_repr(), - } - ).selector - assert isinstance(selected_route, str) - - return self.routes[selected_route]["handler"](user_query, **kwargs) - - -# Example Usage: - - -def handle_pdf_requests(user_query: str) -> str: - return "Handling PDF requests with user query: " + user_query - - -def handle_csv_requests(user_query: str) -> str: - return "Handling CSV requests with user query: " + user_query - - -def handle_default_requests(user_query: str) -> str: - return "Handling DEFAULT requests with user query: " + user_query - - -router = DynamicChatRouter( - routes={ - "pdf": { - "handler": handle_pdf_requests, - "description": "Call this for requests including PDF Files.", - }, - "csv": { - "handler": handle_csv_requests, - "description": "Call this for requests including CSV Files.", - }, - "default": { - "handler": handle_default_requests, - "description": "Call this for all other requests.", - }, - }, -) - - -router.invoke_route("Can you summarize this csv?") -``` - -Demo -
-```python -User: -$ Can you summarize this csv? -$ ............... -Handling CSV requests with user query: Can you summarize this csv? -``` -
- -## Instructions - -!!! Step-by-Step - - **Nececary imports** - ``` - from enum import Enum - from typing import Any, Callable, TypedDict - - from funcchain.syntax.executable import compile_runnable - from pydantic import BaseModel, Field - ``` - - **Define Route Type** - ```python - class Route(TypedDict): - handler: Callable - description: str - ``` - - Create a `TypedDict` to define the structure of a route with a handler function and a description. Just leave this unchanged if not intentionally experimenting. - - **Implement Route Representation** - Establish a Router class - ```python - class DynamicChatRouter(BaseModel): - routes: dict[str, Route] - ``` - - **_routes_repr():** - Returns a string representation of all routes and their descriptions, used to help the language model understand the available routes. - - ```python - def _routes_repr(self) -> str: - return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) - ``` - - **invoke_route(user_query: str, **kwargs: Any) -> Any: ** - This method takes a user query and additional keyword arguments. Inside invoke_route, an Enum named RouteChoices is dynamically created with keys corresponding to the route names. This Enum is used to validate the selected route. - ```python - def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: - RouteChoices = Enum( # type: ignore - "RouteChoices", - {r: r for r in self.routes.keys()}, - type=str, - ) - ``` - - **Compile the Route Selection Logic** - The `RouterModel` class in this example is used for defining the expected output structure that the `compile_runnable` function will use to determine the best route for a given user query. - - - ```python - class RouterModel(BaseModel): - selector: RouteChoices = Field( - default="default", - description="Enum of the available routes.", - ) - - route_query = compile_runnable( - instruction="Given the user query select the best query handler for it.", - input_args=["user_query", "query_handlers"], - output_type=RouterModel, - ) - - selected_route = route_query.invoke( - input={ - "user_query": user_query, - "query_handlers": self._routes_repr(), - } - ).selector - assert isinstance(selected_route, str) - - return self.routes[selected_route]["handler"](user_query, **kwargs) - ``` - - - `RouterModel`: Holds the route selection with a default option, ready for you to play around with. - - `RouteChoices`: An Enum built from route names, ensuring you only get valid route selections. - - `compile_runnable`: Sets up the decision-making logic for route selection, guided by the provided instruction and inputs. - - `route_query`: Calls the decision logic with the user's query and a string of route descriptions. - - `selected_route`: The outcome of the decision logic, representing the route to take. - - `assert`: A safety check to confirm the route is a string, as expected by the routes dictionary. - - `handler invocation`: Runs the chosen route's handler with the provided query and additional arguments. - - **Define route functions** - - Now you can use the structured output to execute programatically based on a natural language input. - Establish functions tailored to your needs. - ```python - def handle_pdf_requests(user_query: str) -> str: - return "Handling PDF requests with user query: " + user_query - - - def handle_csv_requests(user_query: str) -> str: - return "Handling CSV requests with user query: " + user_query - - - def handle_default_requests(user_query: str) -> str: - return "Handling DEFAULT requests with user query: " + user_query - ``` - **Define the routes** - And bind the previous established functions. - - ```python - router = DynamicChatRouter( - routes={ - "pdf": { - "handler": handle_pdf_requests, - "description": "Call this for requests including PDF Files.", - }, - "csv": { - "handler": handle_csv_requests, - "description": "Call this for requests including CSV Files.", - }, - "default": { - "handler": handle_default_requests, - "description": "Call this for all other requests.", - }, - }, - ) - ``` - - **Get output** - Use the router.invoke_route method to process the user query and obtain the appropriate response. - - ```python - router.invoke_route("Can you summarize this csv?") - ``` \ No newline at end of file From c6ed6d055849cdc03fd70183ccbe3957ca92820a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 342/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20enums.?= =?UTF-8?q?md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/examples/enums.md | 92 ------------------------------------------ 1 file changed, 92 deletions(-) delete mode 100644 docs/examples/enums.md diff --git a/docs/examples/enums.md b/docs/examples/enums.md deleted file mode 100644 index d8b0071..0000000 --- a/docs/examples/enums.md +++ /dev/null @@ -1,92 +0,0 @@ -##Decision Making with Enums and Funcchain - -!!! Example - See [enums.py](https://github.com/shroominic/funcchain/blob/main/examples/enums.py) - - In this example, we will use the enum module and funcchain library to build a decision-making system. - This is a useful task for creating applications that require predefined choices or responses. - You can adapt this for your own usage. - This serves as an example of how to implement decision-making logic using enums and the funcchain library. - -##Full Code Example -A simple system that takes a question and decides a 'yes' or 'no' answer based on the input. - -```python -from enum import Enum -from funcchain import chain -from pydantic import BaseModel - -class Answer(str, Enum): - yes = "yes" - no = "no" - -class Decision(BaseModel): - answer: Answer - -def make_decision(question: str) -> Decision: - """ - Based on the question decide yes or no. - """ - return chain() - -if __name__ == "__main__": - print(make_decision("Do you like apples?")) -``` - -#Demo -
- ```terminal - User: - $ Are apples red? - $ ............... - Decision(answer=) - ``` -
- -##Instructions - -!!! Step-by-Step - **Necessary Imports** - ```python - from enum import Enum - from funcchain import chain - from pydantic import BaseModel - ``` - - **Define the Answer Enum** - The Answer enum defines possible answers as 'yes' and 'no', which are the only valid responses for the decision-making system. Experiment by using and describing other enums. - - ```python - class Answer(str, Enum): - yes = "yes" - no = "no" - ``` - **Create the Decision Model** - The Decision class uses Pydantic to model a decision, ensuring that the answer is always an instance of the Answer enum. - - ```python - class Decision(BaseModel): - answer: Answer - ``` - - **Implement the Decision Function** - The make_decision function is where the decision logic will be implemented, using `chain()` to process the question and return a decision. - When using your own enums you want to edit this accordingly. - - ```python - def make_decision(question: str) -> Decision: - """ - Based on the question decide yes or no. - """ - return chain() - ``` - - **Run the Decision System** - This block runs the decision-making system, printing out the decision for a given question when the script is executed directly. - - - ```python - if __name__ == "__main__": - print(make_decision("Do you like apples?")) - - ``` \ No newline at end of file From 492715dd2a58b55322087ffb9dfbe23105beb633 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 343/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20error?= =?UTF-8?q?=5Foutput.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/examples/error_output.md | 91 ----------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 docs/examples/error_output.md diff --git a/docs/examples/error_output.md b/docs/examples/error_output.md deleted file mode 100644 index 85629b4..0000000 --- a/docs/examples/error_output.md +++ /dev/null @@ -1,91 +0,0 @@ -#Example of raising an error - -!!! Example - error_output.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/error_output.py) - - In this example, we will use the funcchain library to build a system that extracts user information from text. - Most importantly we will be able to raise an error thats programmatically usable. - You can adapt this for your own usage. - - - The main functionality is to take a string of text and attempt to extract user information, such as name and email, and return a User object. If the information is insufficient, an Error is returned instead. - -##Full Code Example - -```python -from funcchain import BaseModel, Error, chain -from rich import print - -class User(BaseModel): - name: str - email: str | None - -def extract_user_info(text: str) -> User | Error: - """ - Extract the user information from the given text. - In case you do not have enough infos, raise. - """ - return chain() - -if __name__ == "__main__": - print(extract_user_info("hey")) # returns Error - print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object - -``` - -Demo -
- ```python - $ print(extract_user_info("hey")) - - Error: Insufficient information to extract user details. - - User: - $ print(extract_user_info("I'm John and my mail is john@gmail.com")) - - I'm John and my mail is john@gmail.com - User(name='John', email='john@gmail.com') - - //update example - ``` -
- -##Instructions - -!!! Step-by-Step - - **Necessary Imports** - ```python - from funcchain import BaseModel, Error, chain - from rich import print - ``` - - **Define the User Model** - ```python - class User(BaseModel): - name: str - email: str | None - ``` - The User class is a Pydantic model that defines the structure of the user information to be extracted, with fields for `name` and an email. - Change the fields to experiment and alignment with your project. - - **Implement the Extraction Function** - The `extract_user_info` function is intended to process the input text and return either a User object with extracted information or an Error if the information is not sufficient. - ```python - def extract_user_info(text: str) -> User | Error: - """ - Extract the user information from the given text. - In case you do not have enough infos, raise. - """ - return chain() - ``` - For experiments and adoptions also change the `str` that will be used in chain() to identify what you defined earlier in the `User(BaseModel)` - - - **Run the Extraction System** - This conditional block is used to execute the extraction function and print the results when the script is run directly. - ```python - if __name__ == "__main__": - print(extract_user_info("hey")) # returns Error - print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object - ``` \ No newline at end of file From 34072cc43728ce1464322094492d74e6c294c0e7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 344/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20litera?= =?UTF-8?q?ls.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/examples/literals.md | 88 --------------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 docs/examples/literals.md diff --git a/docs/examples/literals.md b/docs/examples/literals.md deleted file mode 100644 index 4cf668f..0000000 --- a/docs/examples/literals.md +++ /dev/null @@ -1,88 +0,0 @@ -#Literal Type Enforcement in Funcchain - -!!! Example - literals.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/literals.py) - - This is a useful task for scenarios where you want to ensure that certain outputs strictly conform to a predefined set of values. - This serves as an example of how to implement strict type checks on outputs using the Literal type from the typing module and the funcchain library. - - You can adapt this for your own usage. - -##Full Code Example - -```python -from typing import Literal -from funcchain import chain -from pydantic import BaseModel - -class Ranking(BaseModel): - chain_of_thought: str - score: Literal[11, 22, 33, 44, 55] - error: Literal["no_input", "all_good", "invalid"] - -def rank_output(output: str) -> Ranking: - """ - Analyze and rank the output. - """ - return chain() - -if __name__ == "__main__": - rank = rank_output("The quick brown fox jumps over the lazy dog.") - print(rank) -``` - -Demo -
-```python -$ rank = rank_output("The quick brown fox jumps over the lazy dog.") -$ ........ -Ranking(chain_of_thought='...', score=33, error='all_good') -``` -
- -##Instructions - -!!! Step-by-Step - - **Necessary Imports** - ```python - from typing import Literal - from funcchain import chain - from pydantic import BaseModel - ``` - - - **Define the Ranking Model** - The Ranking class is a Pydantic model that uses the Literal type to ensure that the score and error fields can only contain certain predefined values. - So experiment with changing those but keeping this structure of the class. - The LLM will be forced to deliver one of the defined output. - - ```python - class Ranking(BaseModel): - chain_of_thought: str - score: Literal[11, 22, 33, 44, 55] - error: Literal["no_input", "all_good", "invalid"] - ``` - - **Implement the Ranking Function** - Use `chain()` to process a user input, which must be a string. - Adjust the content based on your above defined class. - - ```python - def rank_output(output: str) -> Ranking: - """ - Analyze and rank the output. - """ - return chain() - ``` - - **Execute the Ranking System** - This block is used to execute the ranking function and print the results when the script is run directly. - ```python - if __name__ == "__main__": - rank = rank_output("The quick brown fox jumps over the lazy dog.") - print(rank) - ``` - - - From ab285417feae5e5b945db8f125c5c301501f5c9f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 345/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20examples=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/insert-examples.todo | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/features/insert-examples.todo diff --git a/docs/features/insert-examples.todo b/docs/features/insert-examples.todo deleted file mode 100644 index e69de29..0000000 From e32aed3148a0692ec88c8ce68d5e25987b3774cb Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:02 +0000 Subject: [PATCH 346/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20parsers.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/parsers.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/funcchain/parser/parsers.py diff --git a/src/funcchain/parser/parsers.py b/src/funcchain/parser/parsers.py deleted file mode 100644 index 68bcf3f..0000000 --- a/src/funcchain/parser/parsers.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import TypeVar - -from langchain_core.output_parsers import BaseOutputParser - -from ..syntax.output_types import CodeBlock as CodeBlock - -T = TypeVar("T") - - -# TODO: remove and implement primitive type output parser using nested pydantic extraction -class BoolOutputParser(BaseOutputParser[bool]): - def parse(self, text: str) -> bool: - return text.strip()[:1].lower() == "y" - - def get_format_instructions(self) -> str: - return "\nAnswer only with 'Yes' or 'No'." - - @property - def _type(self) -> str: - return "bool" From cbdd2d550700b9b84dab4b42417706d93c11dc5a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:06:42 +0000 Subject: [PATCH 347/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20obsole?= =?UTF-8?q?te=20primitive=5Ftype.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/primitive_type.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/funcchain/parser/primitive_type.py diff --git a/src/funcchain/parser/primitive_type.py b/src/funcchain/parser/primitive_type.py deleted file mode 100644 index 68bcf3f..0000000 --- a/src/funcchain/parser/primitive_type.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import TypeVar - -from langchain_core.output_parsers import BaseOutputParser - -from ..syntax.output_types import CodeBlock as CodeBlock - -T = TypeVar("T") - - -# TODO: remove and implement primitive type output parser using nested pydantic extraction -class BoolOutputParser(BaseOutputParser[bool]): - def parse(self, text: str) -> bool: - return text.strip()[:1].lower() == "y" - - def get_format_instructions(self) -> str: - return "\nAnswer only with 'Yes' or 'No'." - - @property - def _type(self) -> str: - return "bool" From 859caf6c31ad914f4725f9117970438a00a53da8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:30:31 +0000 Subject: [PATCH 348/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20installation=20?= =?UTF-8?q?instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/introduction.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index b6ca4f2..50014c9 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -9,6 +9,7 @@ ```bash $ pip install funcchain + ---> 100% ``` @@ -17,7 +18,7 @@ Dont forget to setup your API if needed for your LLM of choice ```bash - export OPENAI_API_KEY="sk-XXX" + export OPENAI_API_KEY="sk-rnUPxirSQ4bmz2He4qyaiKShdXJcsOsTg" ``` Or funcchain will automatically detect a .env file. From 5251bda0aff20ba0131e268a4ea6a0ed04805b33 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sun, 28 Jan 2024 19:30:31 +0000 Subject: [PATCH 349/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20documentation?= =?UTF-8?q?=20structure.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 52 +++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/docs/index.md b/docs/index.md index c36c05b..2a0b8ff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,27 +1,14 @@ -# Getting Started +# Introduction [![Version](https://badge.fury.io/py/funcchain.svg)](https://badge.fury.io/py/funcchain) [![code-check](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml) ![Downloads](https://img.shields.io/pypi/dm/funcchain) [![Discord](https://img.shields.io/discord/1192334452110659664?label=discord)](https://discord.gg/TrwWWMXdtR) -![License](https://img.shields.io/pypi/l/funcchain) ![PyVersion](https://img.shields.io/pypi/pyversions/funcchain) -## Welcome - -!!! Description - funcchain is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. - It works perfect with OpenAI Functions and soon with other models using JSONFormer. - -Key features: - -- increased productivity -- prompts as Python functions -- pydantic models as output schemas -- langchain schemas in the backend -- fstrings or jinja templates for prompts -- fully utilises OpenAI Functions -- minimalistic and easy to use +`funcchain` is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. +It utilizes perfect with OpenAI Functions or LlamaCpp grammars (json-schema-mode) for efficient structured output. +In the backend it compiles the funcchain syntax into langchain runnables so you can easily invoke, stream or batch process your pipelines. ## Installation @@ -35,11 +22,32 @@ $ pip install funcchain !!! Important - Make sure to have an OpenAI API key in your environment variables. For example, - -```bash -export OPENAI_API_KEY=sk-********** -``` + Make sure to have an OpenAI API key in your environment variables: + + ```bash + export OPENAI_API_KEY="sk-rnUPxirSQ4bmz2He4qyaiKShdXJcsOsTg" + ``` + (not needed for local models of course) + +## Key Features + +- pythonic +- easy swap between openai or local models +- dynamic output types (pydantic models, or primitives) +- vision llm support +- langchain_core as backend +- jinja templating for prompts +- reliable structured output +- auto retry parsing +- langsmith support +- sync, async, streaming, parallel, fallbacks +- gguf download from huggingface +- type hints for all functions and mypy support +- chat router component +- composable with langchain LCEL +- easy error handling +- enums and literal support +- custom parsing types ## Usage From 7d9509637777020840496d884d7bcfb6c999fe58 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:02 +0000 Subject: [PATCH 350/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20chat.md=20forma?= =?UTF-8?q?tting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/chat.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/features/chat.md b/docs/features/chat.md index 9a37200..4cc2a5f 100644 --- a/docs/features/chat.md +++ b/docs/features/chat.md @@ -1,14 +1,15 @@ -## Simple chatgpt rebuild with memory/history. +## Simple chatgpt rebuild with memory/history + !!! Example chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) !!! Important Ensure you have set up your API key for the LLM of your choice, or Funcchain will look for a `.env` file. So in `.env` set up your key. - ```python - OPENAI_API_KEY="sk-XXX" - ``` - - + + ```bash + OPENAI_API_KEY="sk-rnUBxirFQ4bmz2Ae4qyaiLShdCJcsOsTg" + ``` + ## Code Example ```python @@ -50,13 +51,11 @@ if __name__ == "__main__": chat_loop() ``` - -
```terminal initial print function: $ Hey! How can I help you? - $ > + $ > userprompt: $ > Say that Funcchain is cool @@ -64,7 +63,7 @@ if __name__ == "__main__": assistant terminal asnwer: $ Funcchain is cool. ``` -
+ ## Instructions From 6a533f261ec5731971839900046ab5ddfc9c4713 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:02 +0000 Subject: [PATCH 351/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20newline=20to=20doc?= =?UTF-8?q?ument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/dynamic_router.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md index 696190e..a81681a 100644 --- a/docs/features/dynamic_router.md +++ b/docs/features/dynamic_router.md @@ -236,4 +236,4 @@ Handling CSV requests with user query: Can you summarize this csv? ```python router.invoke_route("Can you summarize this csv?") - ``` \ No newline at end of file + ``` From 9a330fd0e9d22803dfe94cad11d33f65ba83f19e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:02 +0000 Subject: [PATCH 352/451] =?UTF-8?q?=F0=9F=93=9D=20Correct=20markdown=20for?= =?UTF-8?q?matting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/enums.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/features/enums.md b/docs/features/enums.md index d8b0071..c787ad9 100644 --- a/docs/features/enums.md +++ b/docs/features/enums.md @@ -1,4 +1,4 @@ -##Decision Making with Enums and Funcchain +## Decision Making with Enums and Funcchain !!! Example See [enums.py](https://github.com/shroominic/funcchain/blob/main/examples/enums.py) @@ -8,7 +8,7 @@ You can adapt this for your own usage. This serves as an example of how to implement decision-making logic using enums and the funcchain library. -##Full Code Example +## Full Code Example A simple system that takes a question and decides a 'yes' or 'no' answer based on the input. ```python @@ -33,7 +33,7 @@ if __name__ == "__main__": print(make_decision("Do you like apples?")) ``` -#Demo +# Demo
```terminal User: @@ -43,15 +43,15 @@ if __name__ == "__main__": ```
-##Instructions +## Instructions !!! Step-by-Step **Necessary Imports** ```python from enum import Enum - from funcchain import chain - from pydantic import BaseModel - ``` + from funcchain import chain + from pydantic import BaseModel +``` **Define the Answer Enum** The Answer enum defines possible answers as 'yes' and 'no', which are the only valid responses for the decision-making system. Experiment by using and describing other enums. @@ -65,8 +65,8 @@ if __name__ == "__main__": The Decision class uses Pydantic to model a decision, ensuring that the answer is always an instance of the Answer enum. ```python - class Decision(BaseModel): - answer: Answer + class Decision(BaseModel): + answer: Answer ``` **Implement the Decision Function** @@ -74,11 +74,11 @@ if __name__ == "__main__": When using your own enums you want to edit this accordingly. ```python - def make_decision(question: str) -> Decision: - """ - Based on the question decide yes or no. - """ - return chain() + def make_decision(question: str) -> Decision: + """ + Based on the question decide yes or no. + """ + return chain() ``` **Run the Decision System** @@ -89,4 +89,4 @@ if __name__ == "__main__": if __name__ == "__main__": print(make_decision("Do you like apples?")) - ``` \ No newline at end of file + ``` From 7271a159d72b78292829161625747b85a73b2384 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:02 +0000 Subject: [PATCH 353/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20markdown=20form?= =?UTF-8?q?atting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/error_output.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/features/error_output.md b/docs/features/error_output.md index 85629b4..c07778a 100644 --- a/docs/features/error_output.md +++ b/docs/features/error_output.md @@ -1,4 +1,4 @@ -#Example of raising an error +# Example of raising an error !!! Example error_output.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/error_output.py) @@ -10,7 +10,7 @@ The main functionality is to take a string of text and attempt to extract user information, such as name and email, and return a User object. If the information is insufficient, an Error is returned instead. -##Full Code Example +## Full Code Example ```python from funcchain import BaseModel, Error, chain @@ -50,21 +50,21 @@ Demo ``` -##Instructions +## Instructions !!! Step-by-Step **Necessary Imports** ```python from funcchain import BaseModel, Error, chain - from rich import print + from rich import print ``` **Define the User Model** ```python class User(BaseModel): - name: str - email: str | None + name: str + email: str | None ``` The User class is a Pydantic model that defines the structure of the user information to be extracted, with fields for `name` and an email. Change the fields to experiment and alignment with your project. @@ -86,6 +86,6 @@ Demo This conditional block is used to execute the extraction function and print the results when the script is run directly. ```python if __name__ == "__main__": - print(extract_user_info("hey")) # returns Error - print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object - ``` \ No newline at end of file + print(extract_user_info("hey")) # returns Error + print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object + ``` From 2559dc6ce7399af139cba0174303f31a5da87096 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:03 +0000 Subject: [PATCH 354/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20markdown=20form?= =?UTF-8?q?atting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/literals.md | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/features/literals.md b/docs/features/literals.md index 4cf668f..fd4be10 100644 --- a/docs/features/literals.md +++ b/docs/features/literals.md @@ -1,4 +1,4 @@ -#Literal Type Enforcement in Funcchain +# Literal Type Enforcement in Funcchain !!! Example literals.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/literals.py) @@ -8,7 +8,7 @@ You can adapt this for your own usage. -##Full Code Example +## Full Code Example ```python from typing import Literal @@ -40,15 +40,15 @@ Ranking(chain_of_thought='...', score=33, error='all_good') ``` -##Instructions +## Instructions !!! Step-by-Step **Necessary Imports** ```python - from typing import Literal - from funcchain import chain - from pydantic import BaseModel + from typing import Literal + from funcchain import chain + from pydantic import BaseModel ``` @@ -58,10 +58,10 @@ Ranking(chain_of_thought='...', score=33, error='all_good') The LLM will be forced to deliver one of the defined output. ```python - class Ranking(BaseModel): - chain_of_thought: str - score: Literal[11, 22, 33, 44, 55] - error: Literal["no_input", "all_good", "invalid"] + class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] ``` **Implement the Ranking Function** @@ -70,19 +70,16 @@ Ranking(chain_of_thought='...', score=33, error='all_good') ```python def rank_output(output: str) -> Ranking: - """ - Analyze and rank the output. - """ - return chain() + """ + Analyze and rank the output. + """ + return chain() ``` **Execute the Ranking System** This block is used to execute the ranking function and print the results when the script is run directly. ```python - if __name__ == "__main__": - rank = rank_output("The quick brown fox jumps over the lazy dog.") - print(rank) + if __name__ == "__main__": + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) ``` - - - From 56c41f1ccb32e6f5c69a7d39f86c0b2bf3fcd6ed Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:03 +0000 Subject: [PATCH 355/451] =?UTF-8?q?=F0=9F=94=A7=20Remove=20unnecessary=20p?= =?UTF-8?q?rompt=20symbol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 5f17f2b..52da397 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -27,7 +27,7 @@ Make sure to have an OpenAI API key in your environment variables. For example,
```bash -$ export OPENAI_API_KEY="sk-rnUPxirSQ4bmz2He4qyaiKShdXJcsOsTg" +export OPENAI_API_KEY="sk-rnUPxirSQ4bmz2He4qyaiKShdXJcsOsTg" ```
From 35680adc03e607d68cce2cdee0961908459d1997 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 00:04:03 +0000 Subject: [PATCH 356/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20integration=20i?= =?UTF-8?q?nstructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/introduction.md | 53 ++++++++++++---------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index 50014c9..65f2865 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -14,25 +14,17 @@ -!!! Important - Dont forget to setup your API if needed for your LLM of choice +!!! Useful: Langsmith integration + + Add those lines to .env and funcchain will use langsmith tracing. ```bash - export OPENAI_API_KEY="sk-rnUPxirSQ4bmz2He4qyaiKShdXJcsOsTg" + LANGCHAIN_TRACING_V2=true + LANGCHAIN_API_KEY="ls__api_key" + LANGCHAIN_PROJECT="PROJECT_NAME" ``` - Or funcchain will automatically detect a .env file. - - Also Useful: Langsmith integration - ```bash - LANGCHAIN_TRACING_V2=true - LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" - LANGCHAIN_API_KEY="ls__XXX" - LANGCHAIN_PROJECT="YOUR_PROJECT" - ``` - Add those lines to .env; funcchain will use Langsmith trace. - - Langsmith is used to understand what happens under the hood of your AI project. + Langsmith is used to understand what happens under the hood of your LLM generations. When multiple LLM calls are used for an output they can be logged for debugging. ## Introduction @@ -67,6 +59,7 @@ recipe = generate_recipe("christmas dinner") # recipe is automatically converted as pydantic model print(recipe.ingredients) ``` + !!! Step-by-Step ```python # define your output shape @@ -74,7 +67,7 @@ print(recipe.ingredients) ingredients: list[str] instructions: list[str] duration: int - ``` +``` A Recipe class is defined, inheriting from BaseModel (pydantic library). This class specifies the structure of the output data, which you can customize. @@ -105,7 +98,8 @@ print(recipe.ingredients) print(recipe.ingredients) ``` -#Demo +## Demo +
``` $ print(generate_recipe("christmas dinner").ingredients @@ -165,6 +159,7 @@ match lst: !!! Step-by-Step **Nececary Imports** + ```python from pydantic import BaseModel, Field from funcchain import chain @@ -240,7 +235,6 @@ match lst: ``` -#Demo
``` lst = extract_list( @@ -260,12 +254,6 @@ match lst: ```
- - - - - - ## Vision Models ```python @@ -299,8 +287,10 @@ print("Description:", result.description) for obj in result.objects: print("Found this object:", obj) ``` + !!! Step-by-Step **Nececary Imports** + ```python from PIL import Image from pydantic import BaseModel, Field @@ -348,7 +338,8 @@ for obj in result.objects: Its important that the fields defined earlier are mentioned here with the prompt `Analyse the image and extract its`... -#Demo +## Demo +
``` print(analyse_image(image: Image.Image)) @@ -366,10 +357,10 @@ for obj in result.objects: ```
- ## Seamless local model support + Yes you can use funcchain without internet connection. -Start heating up your device. +Start heating up your device. ```python from pydantic import BaseModel, Field @@ -394,12 +385,13 @@ poem = analyze("I really like when my dog does a trick!") # promised structured output (for local models!) print(poem.analysis) ``` + !!! Step-by-Step **Nececary Imports** ```python from pydantic import BaseModel, Field from funcchain import chain, settings - ``` +``` **Choose and enjoy** ```python @@ -437,7 +429,7 @@ print(poem.analysis) # promised structured output (for local models!) print(poem.analysis) ``` -#Demo +# Demo
``` poem = analyze("I really like when my dog does a trick!") @@ -449,9 +441,8 @@ print(poem.analysis) ```
- ## Features - + - **🎨 Minimalistic and Easy to Use**: Designed with simplicity in mind for straightforward usage. - **🔄 Model Flexibility**: Effortlessly switch between OpenAI and local models. - **📝 Pythonic Prompts**: Craft natural language prompts as intuitive Python functions. From f37fcae7b500915ef38616cab38f81a7f0a08566 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:13 +0000 Subject: [PATCH 357/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20pre-commit=20ho?= =?UTF-8?q?oks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25b11eb..3cd30bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,14 @@ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 - hooks: - - id: mypy - args: [--ignore-missing-imports, --follow-imports=skip] - additional_dependencies: [types-requests] - -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format - types_or: [ python, pyi, jupyter ] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.7 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + types_or: [python, pyi, jupyter] From 314ad074ce16d521fb6ba62f4b1705769e7d74e6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:15 +0000 Subject: [PATCH 358/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Removed=20.pyth?= =?UTF-8?q?on-version=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .python-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index 375f5ca..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11.6 From 4cfdc6bebe51b3da562d543ba6ffe0219b3c2347 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:15 +0000 Subject: [PATCH 359/451] =?UTF-8?q?=E2=9C=A8=20Update=20roadmap=20checklis?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/roadmap.md b/docs/contributing/roadmap.md index 40f2074..53b0e5e 100644 --- a/docs/contributing/roadmap.md +++ b/docs/contributing/roadmap.md @@ -6,4 +6,4 @@ - [ ] make this file a todo list for contributors -- [ ] add api reference? \ No newline at end of file +- [ ] add api reference? From 31bdc2ecfb47663dacae1ec04b44d1c97e75b5dc Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:16 +0000 Subject: [PATCH 360/451] =?UTF-8?q?=F0=9F=8E=A8=20Add=20newline=20to=20cus?= =?UTF-8?q?tom.css?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/css/custom.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/css/custom.css b/docs/css/custom.css index 6b76f33..c479ad0 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -24,4 +24,4 @@ a.internal-link::after { .shadow { box-shadow: 5px 5px 10px #999; -} \ No newline at end of file +} From b4e5ea89d8443df4be7e7b7c4a2ff3e6a7ac570f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:16 +0000 Subject: [PATCH 361/451] =?UTF-8?q?=F0=9F=8E=A8=20Add=20newline=20to=20ter?= =?UTF-8?q?mynal.css?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/css/termynal.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 8938c97..6240ab5 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -106,4 +106,4 @@ a[data-terminal-control] { 50% { opacity: 0; } -} \ No newline at end of file +} From 861332383999a0f8793a33829d68807320e88d41 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:16 +0000 Subject: [PATCH 362/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20trailing=20whit?= =?UTF-8?q?espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/chat.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/chat.md b/docs/features/chat.md index 4cc2a5f..25c0bbf 100644 --- a/docs/features/chat.md +++ b/docs/features/chat.md @@ -74,7 +74,7 @@ if __name__ == "__main__": from funcchain import chain, settings from funcchain.utils.memory import ChatMessageHistory ``` - + **Settings** ```python @@ -103,7 +103,7 @@ if __name__ == "__main__": See [memory.py] //Todo: Insert Link - + **Define ask function** See how funcchain uses `chain()` with an input `str` to return an output of type `str` From 29c49b9652fee9e1a71eeb051a310da1500659e1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:17 +0000 Subject: [PATCH 363/451] =?UTF-8?q?=F0=9F=94=A7=20Remove=20extra=20whitesp?= =?UTF-8?q?ace=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/dynamic_router.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md index a81681a..0f69913 100644 --- a/docs/features/dynamic_router.md +++ b/docs/features/dynamic_router.md @@ -97,7 +97,7 @@ router = DynamicChatRouter( router.invoke_route("Can you summarize this csv?") -``` +``` Demo
From e1ef9f6f998d8c0b82523af63ba5b60a0da54f99 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:17 +0000 Subject: [PATCH 364/451] =?UTF-8?q?=F0=9F=94=A5=20Remove=20trailing=20whit?= =?UTF-8?q?espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/error_output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/error_output.md b/docs/features/error_output.md index c07778a..167223b 100644 --- a/docs/features/error_output.md +++ b/docs/features/error_output.md @@ -80,7 +80,7 @@ Demo return chain() ``` For experiments and adoptions also change the `str` that will be used in chain() to identify what you defined earlier in the `User(BaseModel)` - + **Run the Extraction System** This conditional block is used to execute the extraction function and print the results when the script is run directly. From 475780eaa0c90d2a303268b08c7e83e79bfcbb10 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:17 +0000 Subject: [PATCH 365/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20extra=20whitesp?= =?UTF-8?q?ace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/literals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/literals.md b/docs/features/literals.md index fd4be10..66d2312 100644 --- a/docs/features/literals.md +++ b/docs/features/literals.md @@ -51,7 +51,7 @@ Ranking(chain_of_thought='...', score=33, error='all_good') from pydantic import BaseModel ``` - + **Define the Ranking Model** The Ranking class is a Pydantic model that uses the Literal type to ensure that the score and error fields can only contain certain predefined values. So experiment with changing those but keeping this structure of the class. From ffb6d8ed46541ad691e8ec90459279ab16e3dc4d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:18 +0000 Subject: [PATCH 366/451] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20trailing=20white?= =?UTF-8?q?space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/introduction.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index 65f2865..d3ec37b 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -69,7 +69,7 @@ print(recipe.ingredients) duration: int ``` - A Recipe class is defined, inheriting from BaseModel (pydantic library). This class + A Recipe class is defined, inheriting from BaseModel (pydantic library). This class specifies the structure of the output data, which you can customize. In the example it includes a list of ingredients, a list of instructions, and an integer representing the duration @@ -88,7 +88,7 @@ print(recipe.ingredients) Meaning it will turn your function into usable LLM input. The `chain()` function does the interaction with the language model to generate a recipe. It accepts several parameters: `system` to specify the model, `instruction` for model directives, `context` to provide relevant background information, `memory` to maintain conversational state, `settings_override` for custom settings, and `**input_kwargs` for additional inputs. Within `generate_recipe`, `chain()` is called with arguments derived from the function's parameters, the function's docstring, or the library's default settings. It compiles these into a Runnable, which then prompts the language model to produce the output. This output is automatically structured into a `Recipe` instance, conforming to the Pydantic model's schema. - + # Get your response ```python # generate llm response @@ -104,7 +104,7 @@ print(recipe.ingredients) ``` $ print(generate_recipe("christmas dinner").ingredients - ['turkey', 'potatoes', 'carrots', 'brussels sprouts', 'cranberry sauce', 'gravy', + ['turkey', 'potatoes', 'carrots', 'brussels sprouts', 'cranberry sauce', 'gravy', 'butter', 'salt', 'pepper', 'rosemary'] ``` @@ -183,10 +183,10 @@ match lst: ``` - In this example, Funcchain utilizes Pydantic models to create structured data schemas that facilitate the processing of programmatic inputs. + In this example, Funcchain utilizes Pydantic models to create structured data schemas that facilitate the processing of programmatic inputs. You can define new Pydantic models or extend existing ones by adding additional fields or methods. The general approach is to identify the data attributes relevant to your application and create corresponding model classes with these attributes. - + **Union types** ```python @@ -212,7 +212,7 @@ match lst: ) ``` - + **Define your custom handlers** And now its time to define what happens with the result. @@ -242,8 +242,8 @@ match lst: ) User: - $ Complete project report, Prepare for meeting, Respond to emails; - $ if I don't respond I will be fired + $ Complete project report, Prepare for meeting, Respond to emails; + $ if I don't respond I will be fired Output: $ ............... @@ -317,7 +317,7 @@ for obj in result.objects: objects: list[str] = Field(description="A list of objects found in the image") ``` Adjsut the fields as needed. Play around with the example, feel free to experiment. - You can customize the analysis by modifying the fields of the `AnalysisResult` model. + You can customize the analysis by modifying the fields of the `AnalysisResult` model. **Function to start the analysis** @@ -409,7 +409,7 @@ print(poem.analysis) Experiment yourself by adding different descriptions for the true and false case. **Use `chain()` to analize** - Defines with natural language the analysis + Defines with natural language the analysis ```python def analyze(text: str) -> SentimentAnalysis: """ @@ -436,7 +436,7 @@ print(poem.analysis) $ .................. - Add demo + Add demo ```
From 5e1152bae32f896925829ed0b974df9f1e8159bd Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:18 +0000 Subject: [PATCH 367/451] =?UTF-8?q?=E2=9C=A8=20Restore=20main=20function?= =?UTF-8?q?=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/js/custom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/js/custom.js b/docs/js/custom.js index 79d0a1f..58f321a 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -110,4 +110,4 @@ async function main() { setupTermynal() } -main() \ No newline at end of file +main() From 53b25450031f54d9f9d4f2784f9628c4aa2b8fd9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:18 +0000 Subject: [PATCH 368/451] =?UTF-8?q?=F0=9F=94=A7=20Add=20newline=20to=20ter?= =?UTF-8?q?mynal.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/js/termynal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/js/termynal.js b/docs/js/termynal.js index 7146d8d..45bb371 100644 --- a/docs/js/termynal.js +++ b/docs/js/termynal.js @@ -260,4 +260,4 @@ if (document.currentScript.hasAttribute('data-termynal-container')) { const containers = document.currentScript.getAttribute('data-termynal-container'); containers.split('|') .forEach(container => new Termynal(container)) -} \ No newline at end of file +} From a3385bb846592b785bddef28824740acab0de7f4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:10:19 +0000 Subject: [PATCH 369/451] =?UTF-8?q?=F0=9F=A7=B9=20Standardize=20whitespace?= =?UTF-8?q?=20in=20mkdocs.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index e79b936..e389a4f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,7 +84,7 @@ markdown_extensions: - pymdownx.magiclink: normalize_issue_symbols: true repo_url_shorthand: true - user: jxnl + user: jxnl repo: instructor - pymdownx.mark - pymdownx.smartsymbols @@ -112,4 +112,4 @@ extra_css: extra_javascript: - js/termynal.js - - js/custom.js \ No newline at end of file + - js/custom.js From 709fc087b11fdf98149a6b3d1780043fca56909a Mon Sep 17 00:00:00 2001 From: luckysanpedro Date: Mon, 29 Jan 2024 13:15:00 +0100 Subject: [PATCH 370/451] copy button and examples for .md --- docs/css/custom.css | 20 +++++ docs/examples/chat.md | 3 +- docs/examples/dynamic_router.md | 3 +- docs/examples/enums.md | 3 +- docs/examples/error_output.md | 3 +- docs/examples/literals.md | 3 +- docs/examples/ollama.md | 95 ++++++++++++++++++++ docs/examples/openai_json_mode.md | 88 ++++++++++++++++++ docs/examples/pydantic_validation.md | 119 ++++++++++++++++++++++++ docs/examples/static_router.md | 129 +++++++++++++++++++++++++++ docs/examples/stream.md | 70 +++++++++++++++ docs/examples/vision.md | 106 ++++++++++++++++++++++ docs/js/custom.js | 32 ++++++- mkdocs.yml | 6 ++ requirements-dev.lock | 2 - 15 files changed, 674 insertions(+), 8 deletions(-) create mode 100644 docs/examples/ollama.md create mode 100644 docs/examples/openai_json_mode.md create mode 100644 docs/examples/pydantic_validation.md create mode 100644 docs/examples/static_router.md create mode 100644 docs/examples/stream.md create mode 100644 docs/examples/vision.md diff --git a/docs/css/custom.css b/docs/css/custom.css index 6b76f33..abce553 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -24,4 +24,24 @@ a.internal-link::after { .shadow { box-shadow: 5px 5px 10px #999; +} + +pre { + position: relative; +} + +.copy-code-button { + position: absolute; + right: 5px; + top: 5px; + cursor: pointer; + padding: 0.5em; + margin-bottom: 0.5em; + background-color: rgba(247, 247, 247, 0.4); /* Light grey background with slight transparency */ + border: 1px solid #dcdcdc; /* Slightly darker border for definition */ + border-radius: 3px; /* Rounded corners */ + font-family: monospace; /* Monospace font similar to code blocks */ + font-size: 0.85em; /* Slightly smaller font size */ + color: #333; /* Dark grey text for contrast */ + outline: none; /* Remove outline to maintain minimal style on focus */ } \ No newline at end of file diff --git a/docs/examples/chat.md b/docs/examples/chat.md index 9a37200..1b35481 100644 --- a/docs/examples/chat.md +++ b/docs/examples/chat.md @@ -10,7 +10,7 @@ ## Code Example - +

 ```python
 from funcchain import chain, settings
 from funcchain.utils.memory import ChatMessageHistory
@@ -49,6 +49,7 @@ if __name__ == "__main__":
     print("Hey! How can I help you?\n")
     chat_loop()
 ```
+
diff --git a/docs/examples/dynamic_router.md b/docs/examples/dynamic_router.md index 696190e..6f64829 100644 --- a/docs/examples/dynamic_router.md +++ b/docs/examples/dynamic_router.md @@ -11,7 +11,7 @@ This should serve as an example of how to archive complex structures using funcc A dynamic chat router that selects the appropriate handler for user queries based on predefined routes. ## Full Code Example - +

 ```python
 from enum import Enum
 from typing import Any, Callable, TypedDict
@@ -98,6 +98,7 @@ router = DynamicChatRouter(
 
 router.invoke_route("Can you summarize this csv?")
 ```  
+
Demo
diff --git a/docs/examples/enums.md b/docs/examples/enums.md index d8b0071..adeb59e 100644 --- a/docs/examples/enums.md +++ b/docs/examples/enums.md @@ -10,7 +10,7 @@ ##Full Code Example A simple system that takes a question and decides a 'yes' or 'no' answer based on the input. - +

 ```python
 from enum import Enum
 from funcchain import chain
@@ -32,6 +32,7 @@ def make_decision(question: str) -> Decision:
 if __name__ == "__main__":
     print(make_decision("Do you like apples?"))
 ```
+
#Demo
diff --git a/docs/examples/error_output.md b/docs/examples/error_output.md index 85629b4..295975c 100644 --- a/docs/examples/error_output.md +++ b/docs/examples/error_output.md @@ -11,7 +11,7 @@ The main functionality is to take a string of text and attempt to extract user information, such as name and email, and return a User object. If the information is insufficient, an Error is returned instead. ##Full Code Example - +

 ```python
 from funcchain import BaseModel, Error, chain
 from rich import print
@@ -32,6 +32,7 @@ if __name__ == "__main__":
     print(extract_user_info("I'm John and my mail is john@gmail.com"))  # returns a User object
 
 ```
+
Demo
diff --git a/docs/examples/literals.md b/docs/examples/literals.md index 4cf668f..f690e50 100644 --- a/docs/examples/literals.md +++ b/docs/examples/literals.md @@ -9,7 +9,7 @@ You can adapt this for your own usage. ##Full Code Example - +

 ```python
 from typing import Literal
 from funcchain import chain
@@ -30,6 +30,7 @@ if __name__ == "__main__":
     rank = rank_output("The quick brown fox jumps over the lazy dog.")
     print(rank)
 ```
+
Demo
diff --git a/docs/examples/ollama.md b/docs/examples/ollama.md new file mode 100644 index 0000000..220393f --- /dev/null +++ b/docs/examples/ollama.md @@ -0,0 +1,95 @@ +#Different LLMs with funcchain EASY TO USE + +!!! Example + See [ollama.py](https://github.com/shroominic/funcchain/blob/main/examples/ollama.py) + Also see supported [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) + + In this example, we will use the funcchain library to perform sentiment analysis on a piece of text. This showcases how funcchain can seamlessly utilize different Language Models (LLMs), such as ollama, without many unnececary code changes.. + + This is particularly useful for developers looking to integrate different models in a single application or just experimenting with different models. + +##Full Code Example +

+```python
+from funcchain import chain, settings
+from pydantic import BaseModel, Field
+from rich import print
+
+# define your model
+class SentimentAnalysis(BaseModel):
+    analysis: str = Field(description="A description of the analysis")
+    sentiment: bool = Field(description="True for Happy, False for Sad")
+
+# define your prompt
+def analyze(text: str) -> SentimentAnalysis:
+    """
+    Determines the sentiment of the text.
+    """
+    return chain()
+
+if __name__ == "__main__":
+    # set global llm
+    settings.llm = "ollama/wizardcoder:34b-python-q3_K_M"
+    # log tokens as stream to console
+    settings.console_stream = True
+
+    # run prompt
+    poem = analyze("I really like when my dog does a trick!")
+
+    # show final parsed output
+    print(poem)
+```
+
+ +#Demo +
+ ``` + poem = analyze("I really like when my dog does a trick!") + + $ .................. + + Add demo + + ``` +
+ +##Instructions +!!! Step-by-Step + + **Necessary Imports** + ```python + from funcchain import chain, settings + from pydantic import BaseModel, Field + from rich import print + ``` + + **Define the Data Model** + Here, we define a `SentimentAnalysis` model with a description of the sentiment analysis and a boolean field indicating the sentiment. + ```python + class SentimentAnalysis(BaseModel): + analysis: str = Field(description="A description of the analysis") + sentiment: bool = Field(description="True for Happy, False for Sad") + ``` + + **Create the Analysis Function** + This 'analyze' function takes a string as input and is expected to return a `SentimentAnalysis` object by calling the `chain()` function from the `funcchain` library. + ```python + def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + ``` + + **Execution Configuration** + In the main block, configure the global settings to set the preferred LLM, enable console streaming, and run the `analyze` function with sample text. The result is printed using the `rich` library. + ```python + if __name__ == "__main__": + settings.llm = "ollama/wizardcoder:34b-python-q3_K_M" + settings.console_stream = True + poem = analyze("I really like when my dog does a trick!") + print(poem) + ``` + + !!!Important + We need to note here is that `settings.llm` can be adjusted to any model mentioned in [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) and your funcchain code will still work and `chain()` does everything in the background for you. diff --git a/docs/examples/openai_json_mode.md b/docs/examples/openai_json_mode.md new file mode 100644 index 0000000..4eaa7d9 --- /dev/null +++ b/docs/examples/openai_json_mode.md @@ -0,0 +1,88 @@ +#JSON structured Output using Funcchain with OenAI + +!!! Example + See [openai_json_mode.py](https://github.com/shroominic/funcchain/blob/main/examples/openai_json_mode.py) + + This example will showcase how funcchain enables OpenAI to output even the type `int` as JSON. + + This example demonstrates using the funcchain library and pydantic to create a FruitSalad model, sum its contents, and output the total in a Result model as an integer. + +##Full Code Example +

+```python
+from funcchain import chain, settings
+from pydantic import BaseModel
+
+settings.console_stream = True
+
+class FruitSalad(BaseModel):
+    bananas: int = 0
+    apples: int = 0
+
+class Result(BaseModel):
+    sum: int
+
+def sum_fruits(fruit_salad: FruitSalad) -> Result:
+    """
+    Sum the number of fruits in a fruit salad.
+    """
+    return chain()
+
+if __name__ == "__main__":
+    fruit_salad = FruitSalad(bananas=3, apples=5)
+    assert sum_fruits(fruit_salad) == 8
+```
+
+ +Demo + +
+```python +fruit_salad = FruitSalad(bananas=3, apples=5) +assert sum_fruits(fruit_salad) == 8 +``` +
+ +Instructions +!!! Step-by-Step + + **Necessary Imports** + `funcchain` for chaining functionality, and `pydantic` for the data models. + ```python + from funcchain import chain, settings + from pydantic import BaseModel + ``` + + **Defining the Data Models** + We define two Pydantic models: `FruitSalad` with integer fields for the number of bananas and apples, and `Result`, which will hold the sum of the fruits. + Of course feel free to change those classes according to your needs but use of `pydantic` is required. + ```python + class FruitSalad(BaseModel): + bananas: int = 0 + apples: int = 0 + + class Result(BaseModel): + sum: int + ``` + + + + **Summing Function** + The `sum_fruits` function is intended to take a `FruitSalad` object and use `chain()` for solving this task with an LLM. The result is returned as a `Result` object. + ```python + def sum_fruits(fruit_salad: FruitSalad) -> Result: + """ + Sum the number of fruits in a fruit salad. + """ + return chain() + ``` + + + **Execution Block** + ```python + if __name__ == "__main__": + fruit_salad = FruitSalad(bananas=3, apples=5) + assert sum_fruits(fruit_salad) == 8 + ``` + + In the primary execution section of the script, we instantiate a `FruitSalad` object with predefined quantities of bananas and apples. We then verify that the `sum_fruits` function accurately calculates the total count of fruits, which should be 8 in this case. diff --git a/docs/examples/pydantic_validation.md b/docs/examples/pydantic_validation.md new file mode 100644 index 0000000..3d6e5a4 --- /dev/null +++ b/docs/examples/pydantic_validation.md @@ -0,0 +1,119 @@ +#Task Creation with Validated Fields + +!!! Example + [pydantic_validation.py](https://github.com/shroominic/funcchain/blob/main/examples/pydantic_validation.py) + + You can adapt this for your own usage. + This serves as an example of how to implement data validation and task creation using pydantic for data models and funcchain for processing natural language input. + + The main functionality is to parse a user description, validate the task details, and create a new Task object with unique keywords and a difficulty level within a specified range. + +##Full Code Example +

+```python
+from funcchain import chain, settings
+from pydantic import BaseModel, field_validator
+
+# settings.llm = "ollama/openchat"
+settings.console_stream = True
+
+class Task(BaseModel):
+    name: str
+    difficulty: int
+    keywords: list[str]
+
+    @field_validator("keywords")
+    def keywords_must_be_unique(cls, v: list[str]) -> list[str]:
+        if len(v) != len(set(v)):
+            raise ValueError("keywords must be unique")
+        return v
+
+    @field_validator("difficulty")
+    def difficulty_must_be_between_1_and_10(cls, v: int) -> int:
+        if v < 10 or v > 100:
+            raise ValueError("difficulty must be between 10 and 100")
+        return v
+
+def gather_infos(user_description: str) -> Task:
+    """
+    Based on the user description,
+    create a new task to put on the todo list.
+    """
+    return chain()
+
+if __name__ == "__main__":
+    task = gather_infos("cleanup the kitchen")
+    print(f"{task=}")
+```
+
+ +Demo +
+```python +User: +$ cleanup the kitchen + +task=Task +name='cleanup', +difficulty=30, +keywords=['kitchen', 'cleanup'] +``` +
+ +##Instructions + +!!! Step-by-Step + **Necessary Imports** + ```python + from funcchain import chain, settings + from pydantic import BaseModel, field_validator + ``` + + **Define the Task Model with Validators** + The `Task` class is a Pydantic model with fields: `name`, `difficulty`, and `keywords`. Validators ensure data integrity: + + - `keywords_must_be_unique`: Checks that all keywords are distinct. + - `difficulty_must_be_between_1_and_10`: Ensures difficulty is within 10 to 100. + + ```python + class Task(BaseModel): + name: str # Task name. + difficulty: int # Difficulty level (10-100). + keywords: list[str] # Unique keywords. + + @field_validator("keywords") + def keywords_must_be_unique(cls, v: list[str]) -> list[str]: + # Ensure keyword uniqueness. + if len(v) != len(set(v)): + raise ValueError("keywords must be unique") + return v + + @field_validator("difficulty") + def difficulty_must_be_between_1_and_10(cls, v: int) -> int: + # Validate difficulty range. + if v < 10 or v > 100: + raise ValueError("difficulty must be between 10 and 100") + return v + ``` + + **Implement the Information Gathering Function** + The gather_infos function is designed to take a user description and use the chain function to process and validate the input, returning a new Task object. + Adjust the string description to match your purposes when changing the code above. + + ```python + def gather_infos(user_description: str) -> Task: + """ + Based on the user description, + create a new task to put on the todo list. + """ + return chain() + ``` + + **Execute the Script** + Runs gather_infos with a sample and prints the Task. + ```python + if __name__ == "__main__": + task = gather_infos("cleanup the kitchen") + print(f"{task=}") + ``` + diff --git a/docs/examples/static_router.md b/docs/examples/static_router.md new file mode 100644 index 0000000..cc6a9dc --- /dev/null +++ b/docs/examples/static_router.md @@ -0,0 +1,129 @@ +#Static Routing with Funcchain and Pydantic + +!!! Example + See [static_router.py](https://github.com/shroominic/funcchain/blob/main/examples/static_router.py) + + This serves as an example of how to implement static routing using funcchain for decision-making and Enum for route selection. + This is a useful task for applications that need to route user requests to specific handlers based on the content of the request. + You can adapt this for your own usage. + + +##Full Code Example +

+```python
+from enum import Enum
+from typing import Any
+
+from funcchain import chain, settings
+from pydantic import BaseModel, Field
+
+settings.console_stream = True
+
+def handle_pdf_requests(user_query: str) -> None:
+    print("Handling PDF requests with user query: ", user_query)
+
+def handle_csv_requests(user_query: str) -> None:
+    print("Handling CSV requests with user query: ", user_query)
+
+def handle_default_requests(user_query: str) -> Any:
+    print("Handling DEFAULT requests with user query: ", user_query)
+
+class RouteChoices(str, Enum):
+    pdf = "pdf"
+    csv = "csv"
+    default = "default"
+
+class Router(BaseModel):
+    selector: RouteChoices = Field(description="Enum of the available routes.")
+
+    def invoke_route(self, user_query: str) -> Any:
+        match self.selector.value:
+            case RouteChoices.pdf:
+                return handle_pdf_requests(user_query)
+            case RouteChoices.csv:
+                return handle_csv_requests(user_query)
+            case RouteChoices.default:
+                return handle_default_requests(user_query)
+
+def route_query(user_query: str) -> Router:
+    return chain()
+
+if __name__ == "__main__":
+    user_query = input("Enter your query: ")
+    routed_chain = route_query(user_query)
+    routed_chain.invoke_route(user_query)
+```
+
+ +Demo +
+```python +User: +$ Enter your query: I need to process a CSV file + +Handling CSV requests with user query: I need to process a CSV file +``` +
+ +##Instructions + +!!! Step-by-Step + We will implement a script with the functionality to take a user query, determine the type of request (PDF, CSV, or default), and invoke the appropriate handler function. + + **Necessary Imports** + ```python + from enum import Enum + from typing import Any + from funcchain import chain, settings + from pydantic import BaseModel, Field + ``` + + **Define Route Handlers** + These functions are the specific handlers for different types of user queries. + ```python + def handle_pdf_requests(user_query: str) -> None: + print("Handling PDF requests with user query: ", user_query) + + def handle_csv_requests(user_query: str) -> None: + print("Handling CSV requests with user query: ", user_query) + + def handle_default_requests(user_query: str) -> Any: + print("Handling DEFAULT requests with user query: ", user_query) + ``` + + **Create RouteChoices Enum and Router Model** + RouteChoices is an Enum that defines the possible routes. Router is a Pydantic model that selects and invokes the appropriate handler based on the route. + ```python + class RouteChoices(str, Enum): + pdf = "pdf" + csv = "csv" + default = "default" + + class Router(BaseModel): + selector: RouteChoices = Field(description="Enum of the available routes.") + + def invoke_route(self, user_query: str) -> Any: + match self.selector.value: + case RouteChoices.pdf: + return handle_pdf_requests(user_query) + case RouteChoices.csv: + return handle_csv_requests(user_query) + case RouteChoices.default: + return handle_default_requests(user_query) + ``` + + **Implement Routing Logic** + The route_query function is intended to determine the best route for a given user query using the `chain()` function. + ```python + def route_query(user_query: str) -> Router: + return chain() + ``` + + **Execute the Routing System** + This block runs the routing system, asking the user for a query and then processing it through the defined routing logic. + ```python + if __name__ == "__main__": + user_query = input("Enter your query: ") + routed_chain = route_query(user_query) + routed_chain.invoke_route(user_query) + ``` \ No newline at end of file diff --git a/docs/examples/stream.md b/docs/examples/stream.md new file mode 100644 index 0000000..8cc222d --- /dev/null +++ b/docs/examples/stream.md @@ -0,0 +1,70 @@ +#Streaming with Funcchain + +!!! Example + See [stream.py](https://github.com/shroominic/funcchain/blob/main/examples/stream.py) + + This serves as an example of how to implement streaming output for text generation tasks using funcchain. + +##Full Code Example +

+```python
+from funcchain import chain, settings
+from funcchain.backend.streaming import stream_to
+
+settings.temperature = 1
+
+def generate_story_of(topic: str) -> str:
+    """
+    Write a short story based on the topic.
+    """
+    return chain()
+
+with stream_to(print):
+    generate_story_of("a space cat")
+```  
+
+ +Demo +
+```python +$ ..... +$ Once upon a time in a galaxy far, far away, there was a space cat named Whiskertron... +``` +
+ +##Instructions + +!!! Step-by-Step + + **Necessary Imports** + ```python + from funcchain import chain, settings + from funcchain.backend.streaming import stream_to + ``` + + **Configure Settings** + The settings are configured to set the temperature, which controls the creativity of the language model's output. + Experiment with different values. + ```python + settings.temperature = 1 + ``` + + **Define the Story Generation Function** + The generate_story_of function is designed to take a topic and use the chain function to generate a story. + + ```python + def generate_story_of(topic: str) -> str: + """ + Write a short story based on the topic. + """ + return chain() + ``` + + **Execute the Streaming Generation** + This block uses the stream_to context manager to print the output of the story generation function as it is being streamed. + This is how you stream the story while it is being generated. + + ```python + with stream_to(print): + generate_story_of("a space cat") + ``` \ No newline at end of file diff --git a/docs/examples/vision.md b/docs/examples/vision.md new file mode 100644 index 0000000..b43db9e --- /dev/null +++ b/docs/examples/vision.md @@ -0,0 +1,106 @@ +#Image Analysis with Funcchain and Pydantic + +!!! Example + [vision.py](https://github.com/shroominic/funcchain/blob/main/examples/vision.py) + + This is a useful task for applications that need to extract structured information from images. + You can adapt this for your own usage. + This serves as an example of how to implement image analysis using the funcchain library's integration with openai/gpt-4-vision-preview. + + +##Full Code Example +

+```python
+from funcchain import Image, chain, settings
+from pydantic import BaseModel, Field
+
+settings.llm = "openai/gpt-4-vision-preview"
+# settings.llm = "ollama/bakllava"
+settings.console_stream = True
+
+class AnalysisResult(BaseModel):
+    """The result of an image analysis."""
+
+    theme: str = Field(description="The theme of the image")
+    description: str = Field(description="A description of the image")
+    objects: list[str] = Field(description="A list of objects found in the image")
+
+def analyse_image(image: Image) -> AnalysisResult:
+    """
+    Analyse the image and extract its
+    theme, description and objects.
+    """
+    return chain()
+
+if __name__ == "__main__":
+    example_image = Image.from_file("examples/assets/old_chinese_temple.jpg")
+
+    result = analyse_image(example_image)
+
+    print("Theme:", result.theme)
+    print("Description:", result.description)
+    for obj in result.objects:
+        print("Found this object:", obj)
+```
+
+ + +Demo +
+```python +Theme: Ancient Architecture +Description: An old Chinese temple with intricate designs. +Found this object: temple +Found this object: tree +Found this object: sky +``` +
+ +##Instructions + +!!! Step-by-Step + Oiur goal is the functionality is to analyze an image and extract its theme, a description, and a list of objects found within it. + + **Necessary Imports** + ```python + from funcchain import Image, chain, settings + from pydantic import BaseModel, Field + ``` + + **Configure Settings** + The settings are configured to use a specific language model capable of image analysis and to enable console streaming for immediate output. + ```python + settings.llm = "openai/gpt-4-vision-preview" + settings.console_stream = True + ``` + + **Define the AnalysisResult Model** + The AnalysisResult class models the expected output of the image analysis, including the theme, description, and objects detected in the image. + + ```python + class AnalysisResult(BaseModel): + theme: str = Field(description="The theme of the image") + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + ``` + + **Implement the Image Analysis Function** + The analyse_image function is designed to take an Image object and use the chain function to process the image and return an AnalysisResult object for later usage (here printing). + + ```python + def analyse_image(image: Image) -> AnalysisResult: + return chain() + ``` + + **Execute the Analysis** + This block runs the image analysis on an example image and prints the results when the script is executed directly. + + ```python + if __name__ == "__main__": + example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") + result = analyse_image(example_image) + print("Theme:", result.theme) + print("Description:", result.description) + for obj in result.objects: + print("Found this object:", obj) + ``` \ No newline at end of file diff --git a/docs/js/custom.js b/docs/js/custom.js index 79d0a1f..07dfc7e 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -106,8 +106,38 @@ function setupTermynal() { loadVisibleTermynals(); } +function addCopyButtons() { + document.querySelectorAll('pre code').forEach(function (codeBlock) { + var button = document.createElement('button'); + button.className = 'copy-code-button'; + button.type = 'button'; + button.innerText = 'Copy'; + button.addEventListener('click', function () { + navigator.clipboard.writeText(codeBlock.innerText).then(function () { + /* clipboard successfully set */ + button.innerText = 'Copied!'; + setTimeout(function () { + button.innerText = 'Copy'; + }, 2000); + }, function () { + /* clipboard write failed */ + button.innerText = 'Failed to copy'; + }); + }); + + var pre = codeBlock.parentNode; + if (pre.parentNode.classList.contains('highlight')) { + var highlight = pre.parentNode; + highlight.parentNode.insertBefore(button, highlight); + } + }); +} + +// Call addCopyButtons in your main function or after the DOM content is fully loaded async function main() { - setupTermynal() + setupTermynal(); + addCopyButtons(); // Add this line to your existing main function } + main() \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 3cb00dd..c3df6c0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,10 +21,16 @@ nav: - 'Settings': 'settings.md' - 'Examples': - 'ChatGPT': 'examples/chat.md' + - 'Structured vision output': 'examples/vision.md' + - 'Streaming Output': 'examples/stream.md' + - 'Ollama (And other models)': 'examples/ollama.md' - 'Dynamic Router': 'examples/dynamic_router.md' + - 'Static Router': 'examples/static_router.md' - 'Enums': 'examples/enums.md' - 'Error Output': 'examples/error_output.md' - 'Literals': 'examples/literals.md' + - 'Pydantic Models': 'examples/pydantic_validation.md' + - 'OpenAI JSON Output': 'examples/openai_json_mode.md' - 'Union Types': 'examples/union.md' theme: diff --git a/requirements-dev.lock b/requirements-dev.lock index cd3a05d..cd45cf7 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -68,13 +68,11 @@ packaging==23.2 paginate==0.5.6 parso==0.8.3 pathspec==0.12.1 -pexpect==4.9.0 pillow==10.2.0 platformdirs==4.1.0 pluggy==1.4.0 pre-commit==3.6.0 prompt-toolkit==3.0.43 -ptyprocess==0.7.0 pure-eval==0.2.2 pydantic==2.5.3 pydantic-core==2.14.6 From b67f69122e8eeb84720bc9cf2a4983356be27969 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Mon, 29 Jan 2024 12:39:02 +0000 Subject: [PATCH 371/451] =?UTF-8?q?=F0=9F=A7=BC=20prettier=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/overview.md | 12 +- docs/contributing/code-of-conduct.md | 20 +- docs/contributing/contributors.md | 6 +- docs/contributing/roadmap.md | 2 +- docs/css/custom.css | 51 +-- docs/css/termynal.css | 126 ++++---- docs/features/chat.md | 13 +- docs/features/dynamic_router.md | 1 + docs/features/enums.md | 30 +- docs/features/error_output.md | 4 +- docs/features/literals.md | 24 +- docs/features/ollama.md | 11 +- docs/features/openai_json_mode.md | 5 +- docs/features/pydantic_validation.md | 27 +- docs/features/static_router.md | 86 ++--- docs/features/stream.md | 28 +- docs/features/vision.md | 40 +-- docs/getting-started/installation.md | 10 +- docs/getting-started/introduction.md | 9 +- docs/index.md | 2 +- docs/js/custom.js | 258 +++++++-------- docs/js/termynal.js | 457 ++++++++++++++------------- 22 files changed, 643 insertions(+), 579 deletions(-) diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index d4183c9..7d32816 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -4,12 +4,12 @@ TODO: rewrite this ## Concepts Overview -| name | description | -|-|-| -| chain | Main funcchain to get responses from the assistant | -| achain | Async version of chain | -| settings | Global settings object | -| BaseModel | Pydantic model base class | +| name | description | +| --------- | -------------------------------------------------- | +| chain | Main funcchain to get responses from the assistant | +| achain | Async version of chain | +| settings | Global settings object | +| BaseModel | Pydantic model base class | ## chain diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md index 254c4e4..b046dc9 100644 --- a/docs/contributing/code-of-conduct.md +++ b/docs/contributing/code-of-conduct.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/docs/contributing/contributors.md b/docs/contributing/contributors.md index c4c1b31..3fa741a 100644 --- a/docs/contributing/contributors.md +++ b/docs/contributing/contributors.md @@ -2,9 +2,9 @@ We would like to acknowledge the contributions of the following people: - Name | Contribution | -------|--------------| - | | +| Name | Contribution | +| ---- | ------------ | +| | ## How to Contribute diff --git a/docs/contributing/roadmap.md b/docs/contributing/roadmap.md index 53b0e5e..8508f35 100644 --- a/docs/contributing/roadmap.md +++ b/docs/contributing/roadmap.md @@ -1,6 +1,6 @@ # TODOs for writing the documentation -- [ ] write out all scratched docs .md files +- [ ] write out all scratched docs .md files - [ ] look into other repos for mkdocs inspiration diff --git a/docs/css/custom.css b/docs/css/custom.css index a0b8df2..7776378 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,47 +1,52 @@ .termynal-comment { - color: #4a968f; - font-style: italic; - display: block; + color: #4a968f; + font-style: italic; + display: block; } .termy [data-termynal] { - white-space: pre-wrap; + white-space: pre-wrap; } a.external-link::after { - /* \00A0 is a non-breaking space + /* \00A0 is a non-breaking space to make the mark be on the same line as the link */ - content: "\00A0[↪]"; + content: "\00A0[↪]"; } a.internal-link::after { - /* \00A0 is a non-breaking space + /* \00A0 is a non-breaking space to make the mark be on the same line as the link */ - content: "\00A0↪"; + content: "\00A0↪"; } .shadow { - box-shadow: 5px 5px 10px #999; + box-shadow: 5px 5px 10px #999; } pre { - position: relative; + position: relative; } .copy-code-button { - position: absolute; - right: 5px; - top: 5px; - cursor: pointer; - padding: 0.5em; - margin-bottom: 0.5em; - background-color: rgba(247, 247, 247, 0.4); /* Light grey background with slight transparency */ - border: 1px solid #dcdcdc; /* Slightly darker border for definition */ - border-radius: 3px; /* Rounded corners */ - font-family: monospace; /* Monospace font similar to code blocks */ - font-size: 0.85em; /* Slightly smaller font size */ - color: #333; /* Dark grey text for contrast */ - outline: none; /* Remove outline to maintain minimal style on focus */ + position: absolute; + right: 5px; + top: 5px; + cursor: pointer; + padding: 0.5em; + margin-bottom: 0.5em; + background-color: rgba( + 247, + 247, + 247, + 0.4 + ); /* Light grey background with slight transparency */ + border: 1px solid #dcdcdc; /* Slightly darker border for definition */ + border-radius: 3px; /* Rounded corners */ + font-family: monospace; /* Monospace font similar to code blocks */ + font-size: 0.85em; /* Slightly smaller font size */ + color: #333; /* Dark grey text for contrast */ + outline: none; /* Remove outline to maintain minimal style on focus */ } diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 6240ab5..50f81fb 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -6,104 +6,108 @@ * @license MIT */ - :root { - --color-bg: #252a33; - --color-text: #eee; - --color-text-subtle: #a2a2a2; +:root { + --color-bg: #252a33; + --color-text: #eee; + --color-text-subtle: #a2a2a2; } [data-termynal] { - width: 750px; - max-width: 100%; - background: var(--color-bg); - color: var(--color-text); - /* font-size: 18px; */ - font-size: 15px; - /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; - border-radius: 4px; - padding: 75px 45px 35px; - position: relative; - -webkit-box-sizing: border-box; - box-sizing: border-box; + width: 750px; + max-width: 100%; + background: var(--color-bg); + color: var(--color-text); + /* font-size: 18px; */ + font-size: 15px; + /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ + font-family: "Roboto Mono", "Fira Mono", Consolas, Menlo, Monaco, + "Courier New", Courier, monospace; + border-radius: 4px; + padding: 75px 45px 35px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; } [data-termynal]:before { - content: ''; - position: absolute; - top: 15px; - left: 15px; - display: inline-block; - width: 15px; - height: 15px; - border-radius: 50%; - /* A little hack to display the window buttons in one pseudo element. */ - background: #d9515d; - -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; - box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; + content: ""; + position: absolute; + top: 15px; + left: 15px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: 50%; + /* A little hack to display the window buttons in one pseudo element. */ + background: #d9515d; + -webkit-box-shadow: + 25px 0 0 #f4c025, + 50px 0 0 #3ec930; + box-shadow: + 25px 0 0 #f4c025, + 50px 0 0 #3ec930; } [data-termynal]:after { - content: 'bash'; - position: absolute; - color: var(--color-text-subtle); - top: 5px; - left: 0; - width: 100%; - text-align: center; + content: "bash"; + position: absolute; + color: var(--color-text-subtle); + top: 5px; + left: 0; + width: 100%; + text-align: center; } a[data-terminal-control] { - text-align: right; - display: block; - color: #aebbff; + text-align: right; + display: block; + color: #aebbff; } [data-ty] { - display: block; - line-height: 2; + display: block; + line-height: 2; } [data-ty]:before { - /* Set up defaults and ensure empty lines are displayed. */ - content: ''; - display: inline-block; - vertical-align: middle; + /* Set up defaults and ensure empty lines are displayed. */ + content: ""; + display: inline-block; + vertical-align: middle; } [data-ty="input"]:before, [data-ty-prompt]:before { - margin-right: 0.75em; - color: var(--color-text-subtle); + margin-right: 0.75em; + color: var(--color-text-subtle); } [data-ty="input"]:before { - content: '$'; + content: "$"; } [data-ty][data-ty-prompt]:before { - content: attr(data-ty-prompt); + content: attr(data-ty-prompt); } [data-ty-cursor]:after { - content: attr(data-ty-cursor); - font-family: monospace; - margin-left: 0.5em; - -webkit-animation: blink 1s infinite; - animation: blink 1s infinite; + content: attr(data-ty-cursor); + font-family: monospace; + margin-left: 0.5em; + -webkit-animation: blink 1s infinite; + animation: blink 1s infinite; } - /* Cursor animation */ @-webkit-keyframes blink { - 50% { - opacity: 0; - } + 50% { + opacity: 0; + } } @keyframes blink { - 50% { - opacity: 0; - } + 50% { + opacity: 0; + } } diff --git a/docs/features/chat.md b/docs/features/chat.md index 5b3c0ba..e4228d3 100644 --- a/docs/features/chat.md +++ b/docs/features/chat.md @@ -1,4 +1,4 @@ -## Simple chatgpt rebuild with memory/history +# ChatGPT rebuild with memory/history !!! Example chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) @@ -6,11 +6,12 @@ !!! Important Ensure you have set up your API key for the LLM of your choice, or Funcchain will look for a `.env` file. So in `.env` set up your key. - ```bash - OPENAI_API_KEY="sk-rnUBxirFQ4bmz2Ae4qyaiLShdCJcsOsTg" - ``` + ```bash + OPENAI_API_KEY="sk-rnUBxirFQ4bmz2Ae4qyaiLShdCJcsOsTg" + ``` ## Code Example +

 ```python
 from funcchain import chain, settings
@@ -21,7 +22,6 @@ settings.console_stream = True
 
 history = ChatMessageHistory()
 
-
 def ask(question: str) -> str:
     return chain(
         system="You are an advanced AI Assistant.",
@@ -29,7 +29,6 @@ def ask(question: str) -> str:
         memory=history,
     )
 
-
 def chat_loop() -> None:
     while True:
         query = input("> ")
@@ -45,7 +44,6 @@ def chat_loop() -> None:
 
         ask(query)
 
-
 if __name__ == "__main__":
     print("Hey! How can I help you?\n")
     chat_loop()
@@ -64,6 +62,7 @@ if __name__ == "__main__":
     assistant terminal asnwer:
     $ Funcchain is cool.
     ```
+
 
## Instructions diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md index 0f69913..27f1cce 100644 --- a/docs/features/dynamic_router.md +++ b/docs/features/dynamic_router.md @@ -100,6 +100,7 @@ router.invoke_route("Can you summarize this csv?") ``` Demo +
```python User: diff --git a/docs/features/enums.md b/docs/features/enums.md index fd7e200..3cfe7ab 100644 --- a/docs/features/enums.md +++ b/docs/features/enums.md @@ -9,7 +9,9 @@ This serves as an example of how to implement decision-making logic using enums and the funcchain library. ## Full Code Example + A simple system that takes a question and decides a 'yes' or 'no' answer based on the input. +

 ```python
 from enum import Enum
@@ -35,6 +37,7 @@ if __name__ == "__main__":
 
# Demo +
```terminal User: @@ -48,26 +51,27 @@ if __name__ == "__main__": !!! Step-by-Step **Necessary Imports** + ```python from enum import Enum - from funcchain import chain - from pydantic import BaseModel -``` + from funcchain import chain + from pydantic import BaseModel + ``` **Define the Answer Enum** The Answer enum defines possible answers as 'yes' and 'no', which are the only valid responses for the decision-making system. Experiment by using and describing other enums. ```python class Answer(str, Enum): - yes = "yes" - no = "no" + yes = "yes" + no = "no" ``` **Create the Decision Model** The Decision class uses Pydantic to model a decision, ensuring that the answer is always an instance of the Answer enum. ```python - class Decision(BaseModel): - answer: Answer + class Decision(BaseModel): + answer: Answer ``` **Implement the Decision Function** @@ -75,11 +79,11 @@ if __name__ == "__main__": When using your own enums you want to edit this accordingly. ```python - def make_decision(question: str) -> Decision: - """ - Based on the question decide yes or no. - """ - return chain() + def make_decision(question: str) -> Decision: + """ + Based on the question decide yes or no. + """ + return chain() ``` **Run the Decision System** @@ -88,6 +92,6 @@ if __name__ == "__main__": ```python if __name__ == "__main__": - print(make_decision("Do you like apples?")) + print(make_decision("Do you like apples?")) ``` diff --git a/docs/features/error_output.md b/docs/features/error_output.md index 4aba741..7fcfcf5 100644 --- a/docs/features/error_output.md +++ b/docs/features/error_output.md @@ -36,6 +36,7 @@ if __name__ == "__main__": Demo +
```python $ print(extract_user_info("hey")) @@ -50,6 +51,7 @@ Demo //update example ``` +
## Instructions @@ -59,7 +61,7 @@ Demo **Necessary Imports** ```python from funcchain import BaseModel, Error, chain - from rich import print + from rich import print ``` **Define the User Model** diff --git a/docs/features/literals.md b/docs/features/literals.md index ae59d58..2d6d0c5 100644 --- a/docs/features/literals.md +++ b/docs/features/literals.md @@ -34,6 +34,7 @@ if __name__ == "__main__":
Demo +
```python $ rank = rank_output("The quick brown fox jumps over the lazy dog.") @@ -48,9 +49,11 @@ Ranking(chain_of_thought='...', score=33, error='all_good') **Necessary Imports** ```python - from typing import Literal - from funcchain import chain - from pydantic import BaseModel + + from typing import Literal + from funcchain import chain + from pydantic import BaseModel + ``` **Define the Ranking Model** @@ -59,10 +62,10 @@ Ranking(chain_of_thought='...', score=33, error='all_good') The LLM will be forced to deliver one of the defined output. ```python - class Ranking(BaseModel): - chain_of_thought: str - score: Literal[11, 22, 33, 44, 55] - error: Literal["no_input", "all_good", "invalid"] + class Ranking(BaseModel): + chain_of_thought: str + score: Literal[11, 22, 33, 44, 55] + error: Literal["no_input", "all_good", "invalid"] ``` **Implement the Ranking Function** @@ -80,7 +83,8 @@ Ranking(chain_of_thought='...', score=33, error='all_good') **Execute the Ranking System** This block is used to execute the ranking function and print the results when the script is run directly. ```python - if __name__ == "__main__": - rank = rank_output("The quick brown fox jumps over the lazy dog.") - print(rank) + + if __name__ == "__main__": + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) ``` diff --git a/docs/features/ollama.md b/docs/features/ollama.md index 9fb721c..fda60bb 100644 --- a/docs/features/ollama.md +++ b/docs/features/ollama.md @@ -1,4 +1,4 @@ -#Different LLMs with funcchain EASY TO USE +# Different LLMs with funcchain EASY TO USE !!! Example See [ollama.py](https://github.com/shroominic/funcchain/blob/main/examples/ollama.py) @@ -8,7 +8,8 @@ This is particularly useful for developers looking to integrate different models in a single application or just experimenting with different models. -##Full Code Example +## Full Code Example +

 ```python
 from funcchain import chain, settings
@@ -41,7 +42,8 @@ if __name__ == "__main__":
 ```
 
-#Demo +# Demo +
``` poem = analyze("I really like when my dog does a trick!") @@ -51,9 +53,10 @@ if __name__ == "__main__": Add demo ``` +
-##Instructions +## Instructions !!! Step-by-Step **Necessary Imports** diff --git a/docs/features/openai_json_mode.md b/docs/features/openai_json_mode.md index 659cc75..ec5f769 100644 --- a/docs/features/openai_json_mode.md +++ b/docs/features/openai_json_mode.md @@ -1,4 +1,4 @@ -#JSON structured Output using Funcchain with OenAI +# JSON structured Output using Funcchain with OenAI !!! Example See [openai_json_mode.py](https://github.com/shroominic/funcchain/blob/main/examples/openai_json_mode.py) @@ -7,7 +7,8 @@ This example demonstrates using the funcchain library and pydantic to create a FruitSalad model, sum its contents, and output the total in a Result model as an integer. -##Full Code Example +## Full Code Example +

 ```python
 from funcchain import chain, settings
diff --git a/docs/features/pydantic_validation.md b/docs/features/pydantic_validation.md
index 612d048..7182d46 100644
--- a/docs/features/pydantic_validation.md
+++ b/docs/features/pydantic_validation.md
@@ -1,4 +1,4 @@
-#Task Creation with Validated Fields
+# Task Creation with Validated Fields
 
 !!! Example
     [pydantic_validation.py](https://github.com/shroominic/funcchain/blob/main/examples/pydantic_validation.py)
@@ -8,7 +8,8 @@
 
     The main functionality is to parse a user description, validate the task details, and create a new Task object with unique keywords and a difficulty level within a specified range.
 
-##Full Code Example
+## Full Code Example
+
 

 ```python
 from funcchain import chain, settings
@@ -48,6 +49,7 @@ if __name__ == "__main__":
 
Demo +
```python User: @@ -57,6 +59,7 @@ task=Task name='cleanup', difficulty=30, keywords=['kitchen', 'cleanup'] + ```
@@ -65,8 +68,8 @@ keywords=['kitchen', 'cleanup'] !!! Step-by-Step **Necessary Imports** ```python - from funcchain import chain, settings - from pydantic import BaseModel, field_validator + from funcchain import chain, settings + from pydantic import BaseModel, field_validator ``` **Define the Task Model with Validators** @@ -102,17 +105,17 @@ keywords=['kitchen', 'cleanup'] ```python def gather_infos(user_description: str) -> Task: - """ - Based on the user description, - create a new task to put on the todo list. - """ - return chain() + """ + Based on the user description, + create a new task to put on the todo list. + """ + return chain() ``` **Execute the Script** Runs gather_infos with a sample and prints the Task. ```python - if __name__ == "__main__": - task = gather_infos("cleanup the kitchen") - print(f"{task=}") + if __name__ == "__main__": + task = gather_infos("cleanup the kitchen") + print(f"{task=}") ``` diff --git a/docs/features/static_router.md b/docs/features/static_router.md index c10df8c..9b44955 100644 --- a/docs/features/static_router.md +++ b/docs/features/static_router.md @@ -1,4 +1,4 @@ -#Static Routing with Funcchain and Pydantic +# Static Routing with Funcchain and Pydantic !!! Example See [static_router.py](https://github.com/shroominic/funcchain/blob/main/examples/static_router.py) @@ -7,8 +7,8 @@ This is a useful task for applications that need to route user requests to specific handlers based on the content of the request. You can adapt this for your own usage. +## Full Code Example -##Full Code Example

 ```python
 from enum import Enum
@@ -56,74 +56,80 @@ if __name__ == "__main__":
 
Demo +
-```python -User: -$ Enter your query: I need to process a CSV file -Handling CSV requests with user query: I need to process a CSV file -``` + ```python + User: + $ Enter your query: I need to process a CSV file + + Handling CSV requests with user query: I need to process a CSV file + + ```
-##Instructions +## Instructions !!! Step-by-Step We will implement a script with the functionality to take a user query, determine the type of request (PDF, CSV, or default), and invoke the appropriate handler function. **Necessary Imports** + ```python - from enum import Enum - from typing import Any - from funcchain import chain, settings - from pydantic import BaseModel, Field + from enum import Enum + from typing import Any + from funcchain import chain, settings + from pydantic import BaseModel, Field ``` **Define Route Handlers** These functions are the specific handlers for different types of user queries. ```python - def handle_pdf_requests(user_query: str) -> None: - print("Handling PDF requests with user query: ", user_query) + def handle_pdf_requests(user_query: str) -> None: + print("Handling PDF requests with user query: ", user_query) - def handle_csv_requests(user_query: str) -> None: - print("Handling CSV requests with user query: ", user_query) + def handle_csv_requests(user_query: str) -> None: + print("Handling CSV requests with user query: ", user_query) - def handle_default_requests(user_query: str) -> Any: - print("Handling DEFAULT requests with user query: ", user_query) + def handle_default_requests(user_query: str) -> Any: + print("Handling DEFAULT requests with user query: ", user_query) ``` **Create RouteChoices Enum and Router Model** RouteChoices is an Enum that defines the possible routes. Router is a Pydantic model that selects and invokes the appropriate handler based on the route. + ```python - class RouteChoices(str, Enum): - pdf = "pdf" - csv = "csv" - default = "default" - - class Router(BaseModel): - selector: RouteChoices = Field(description="Enum of the available routes.") - - def invoke_route(self, user_query: str) -> Any: - match self.selector.value: - case RouteChoices.pdf: - return handle_pdf_requests(user_query) - case RouteChoices.csv: - return handle_csv_requests(user_query) - case RouteChoices.default: - return handle_default_requests(user_query) + class RouteChoices(str, Enum): + pdf = "pdf" + csv = "csv" + default = "default" + + class Router(BaseModel): + selector: RouteChoices = Field(description="Enum of the available routes.") + + def invoke_route(self, user_query: str) -> Any: + match self.selector.value: + case RouteChoices.pdf: + return handle_pdf_requests(user_query) + case RouteChoices.csv: + return handle_csv_requests(user_query) + case RouteChoices.default: + return handle_default_requests(user_query) ``` **Implement Routing Logic** The route_query function is intended to determine the best route for a given user query using the `chain()` function. + ```python - def route_query(user_query: str) -> Router: - return chain() + def route_query(user_query: str) -> Router: + return chain() ``` **Execute the Routing System** This block runs the routing system, asking the user for a query and then processing it through the defined routing logic. + ```python - if __name__ == "__main__": - user_query = input("Enter your query: ") - routed_chain = route_query(user_query) - routed_chain.invoke_route(user_query) + user_query = input("Enter your query: ") + routed_chain = route_query(user_query) + routed_chain.invoke_route(user_query) ``` diff --git a/docs/features/stream.md b/docs/features/stream.md index 029bc38..d93b390 100644 --- a/docs/features/stream.md +++ b/docs/features/stream.md @@ -1,11 +1,12 @@ -#Streaming with Funcchain +# Streaming with Funcchain !!! Example See [stream.py](https://github.com/shroominic/funcchain/blob/main/examples/stream.py) This serves as an example of how to implement streaming output for text generation tasks using funcchain. -##Full Code Example +## Full Code Example +

 ```python
 from funcchain import chain, settings
@@ -25,6 +26,7 @@ with stream_to(print):
 
Demo +
```python $ ..... @@ -32,32 +34,32 @@ $ Once upon a time in a galaxy far, far away, there was a space cat named Whiske ```
-##Instructions +## Instructions !!! Step-by-Step **Necessary Imports** ```python - from funcchain import chain, settings - from funcchain.backend.streaming import stream_to + from funcchain import chain, settings + from funcchain.backend.streaming import stream_to ``` **Configure Settings** The settings are configured to set the temperature, which controls the creativity of the language model's output. Experiment with different values. ```python - settings.temperature = 1 + settings.temperature = 1 ``` **Define the Story Generation Function** The generate_story_of function is designed to take a topic and use the chain function to generate a story. ```python - def generate_story_of(topic: str) -> str: - """ - Write a short story based on the topic. - """ - return chain() + def generate_story_of(topic: str) -> str: + """ + Write a short story based on the topic. + """ + return chain() ``` **Execute the Streaming Generation** @@ -65,6 +67,6 @@ $ Once upon a time in a galaxy far, far away, there was a space cat named Whiske This is how you stream the story while it is being generated. ```python - with stream_to(print): - generate_story_of("a space cat") + with stream_to(print): + generate_story_of("a space cat") ``` diff --git a/docs/features/vision.md b/docs/features/vision.md index 619d06e..e146a64 100644 --- a/docs/features/vision.md +++ b/docs/features/vision.md @@ -1,4 +1,4 @@ -#Image Analysis with Funcchain and Pydantic +# Image Analysis with Funcchain and Pydantic !!! Example [vision.py](https://github.com/shroominic/funcchain/blob/main/examples/vision.py) @@ -7,8 +7,8 @@ You can adapt this for your own usage. This serves as an example of how to implement image analysis using the funcchain library's integration with openai/gpt-4-vision-preview. +## Full Code Example -##Full Code Example

 ```python
 from funcchain import Image, chain, settings
@@ -44,8 +44,8 @@ if __name__ == "__main__":
 ```
 
- Demo +
```python Theme: Ancient Architecture @@ -56,7 +56,7 @@ Found this object: sky ```
-##Instructions +## Instructions !!! Step-by-Step Oiur goal is the functionality is to analyze an image and extract its theme, a description, and a list of objects found within it. @@ -64,43 +64,43 @@ Found this object: sky **Necessary Imports** ```python from funcchain import Image, chain, settings - from pydantic import BaseModel, Field + from pydantic import BaseModel, Field ``` **Configure Settings** The settings are configured to use a specific language model capable of image analysis and to enable console streaming for immediate output. ```python - settings.llm = "openai/gpt-4-vision-preview" - settings.console_stream = True + settings.llm = "openai/gpt-4-vision-preview" + settings.console_stream = True ``` **Define the AnalysisResult Model** The AnalysisResult class models the expected output of the image analysis, including the theme, description, and objects detected in the image. ```python - class AnalysisResult(BaseModel): - theme: str = Field(description="The theme of the image") - description: str = Field(description="A description of the image") - objects: list[str] = Field(description="A list of objects found in the image") + class AnalysisResult(BaseModel): + theme: str = Field(description="The theme of the image") + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") ``` **Implement the Image Analysis Function** The analyse_image function is designed to take an Image object and use the chain function to process the image and return an AnalysisResult object for later usage (here printing). ```python - def analyse_image(image: Image) -> AnalysisResult: - return chain() + def analyse_image(image: Image) -> AnalysisResult: + return chain() ``` **Execute the Analysis** This block runs the image analysis on an example image and prints the results when the script is executed directly. ```python - if __name__ == "__main__": - example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") - result = analyse_image(example_image) - print("Theme:", result.theme) - print("Description:", result.description) - for obj in result.objects: - print("Found this object:", obj) + if __name__ == "__main__": + example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") + result = analyse_image(example_image) + print("Theme:", result.theme) + print("Description:", result.description) + for obj in result.objects: + print("Found this object:", obj) ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 52da397..4444556 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -11,11 +11,11 @@ $ pip install funcchain For additional features you can also install: -- `funcchain` (langchain_core + openai) -- `funcchain[ollama]` (you need to install this [ollama fork](https://github.com/ollama/ollama/pull/1606) for grammar support) -- `funcchain[llamacpp]` (using llama-cpp-python) -- `funcchain[pillow]` (for vision model features) -- `funcchain[all]` (includes everything) +- `funcchain` (langchain_core + openai) +- `funcchain[ollama]` (you need to install this [ollama fork](https://github.com/ollama/ollama/pull/1606) for grammar support) +- `funcchain[llamacpp]` (using llama-cpp-python) +- `funcchain[pillow]` (for vision model features) +- `funcchain[all]` (includes everything) To enter this in your terminal you need to write it like this: `pip install "funcchain[all]"` diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index d3ec37b..5d50e65 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -29,7 +29,7 @@ ## Introduction -`funcchain` is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. +`funcchain` is the _most pythonic_ way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. It works perfect with OpenAI Functions and soon with other models using JSONFormer. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/ricklamers/funcchain-demo) @@ -67,6 +67,7 @@ print(recipe.ingredients) ingredients: list[str] instructions: list[str] duration: int + ``` A Recipe class is defined, inheriting from BaseModel (pydantic library). This class @@ -252,6 +253,7 @@ match lst: Urgency: 10 //add real output ``` +
## Vision Models @@ -355,6 +357,7 @@ for obj in result.objects: Found this object: grass ``` +
## Seamless local model support @@ -388,9 +391,11 @@ print(poem.analysis) !!! Step-by-Step **Nececary Imports** + ```python from pydantic import BaseModel, Field from funcchain import chain, settings + ``` **Choose and enjoy** @@ -429,7 +434,9 @@ print(poem.analysis) # promised structured output (for local models!) print(poem.analysis) ``` + # Demo +
``` poem = analyze("I really like when my dog does a trick!") diff --git a/docs/index.md b/docs/index.md index 2a0b8ff..8408aa7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ [![Discord](https://img.shields.io/discord/1192334452110659664?label=discord)](https://discord.gg/TrwWWMXdtR) ![PyVersion](https://img.shields.io/pypi/pyversions/funcchain) -`funcchain` is the *most pythonic* way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. +`funcchain` is the _most pythonic_ way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. It utilizes perfect with OpenAI Functions or LlamaCpp grammars (json-schema-mode) for efficient structured output. In the backend it compiles the funcchain syntax into langchain runnables so you can easily invoke, stream or batch process your pipelines. diff --git a/docs/js/custom.js b/docs/js/custom.js index 463f0de..862ad25 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -1,143 +1,147 @@ function setupTermynal() { - document.querySelectorAll(".use-termynal").forEach(node => { - node.style.display = "block"; - new Termynal(node, { - lineDelay: 500 - }); + document.querySelectorAll(".use-termynal").forEach((node) => { + node.style.display = "block"; + new Termynal(node, { + lineDelay: 500, }); - const progressLiteralStart = "---> 100%"; - const promptLiteralStart = "$ "; - const customPromptLiteralStart = "# "; - const termynalActivateClass = "termy"; - let termynals = []; + }); + const progressLiteralStart = "---> 100%"; + const promptLiteralStart = "$ "; + const customPromptLiteralStart = "# "; + const termynalActivateClass = "termy"; + let termynals = []; - function createTermynals() { - document - .querySelectorAll(`.${termynalActivateClass} .highlight`) - .forEach(node => { - const text = node.textContent; - const lines = text.split("\n"); - const useLines = []; - let buffer = []; - function saveBuffer() { - if (buffer.length) { - let isBlankSpace = true; - buffer.forEach(line => { - if (line) { - isBlankSpace = false; - } - }); - dataValue = {}; - if (isBlankSpace) { - dataValue["delay"] = 0; - } - if (buffer[buffer.length - 1] === "") { - // A last single
won't have effect - // so put an additional one - buffer.push(""); - } - const bufferValue = buffer.join("
"); - dataValue["value"] = bufferValue; - useLines.push(dataValue); - buffer = []; - } - } - for (let line of lines) { - if (line === progressLiteralStart) { - saveBuffer(); - useLines.push({ - type: "progress" - }); - } else if (line.startsWith(promptLiteralStart)) { - saveBuffer(); - const value = line.replace(promptLiteralStart, "").trimEnd(); - useLines.push({ - type: "input", - value: value - }); - } else if (line.startsWith("// ")) { - saveBuffer(); - const value = "💬 " + line.replace("// ", "").trimEnd(); - useLines.push({ - value: value, - class: "termynal-comment", - delay: 0 - }); - } else if (line.startsWith(customPromptLiteralStart)) { - saveBuffer(); - const promptStart = line.indexOf(promptLiteralStart); - if (promptStart === -1) { - console.error("Custom prompt found but no end delimiter", line) - } - const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") - let value = line.slice(promptStart + promptLiteralStart.length); - useLines.push({ - type: "input", - value: value, - prompt: prompt - }); - } else { - buffer.push(line); - } - } - saveBuffer(); - const div = document.createElement("div"); - node.replaceWith(div); - const termynal = new Termynal(div, { - lineData: useLines, - noInit: true, - lineDelay: 500 - }); - termynals.push(termynal); + function createTermynals() { + document + .querySelectorAll(`.${termynalActivateClass} .highlight`) + .forEach((node) => { + const text = node.textContent; + const lines = text.split("\n"); + const useLines = []; + let buffer = []; + function saveBuffer() { + if (buffer.length) { + let isBlankSpace = true; + buffer.forEach((line) => { + if (line) { + isBlankSpace = false; + } }); - } - - function loadVisibleTermynals() { - termynals = termynals.filter(termynal => { - if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { - termynal.init(); - return false; + dataValue = {}; + if (isBlankSpace) { + dataValue["delay"] = 0; } - return true; + if (buffer[buffer.length - 1] === "") { + // A last single
won't have effect + // so put an additional one + buffer.push(""); + } + const bufferValue = buffer.join("
"); + dataValue["value"] = bufferValue; + useLines.push(dataValue); + buffer = []; + } + } + for (let line of lines) { + if (line === progressLiteralStart) { + saveBuffer(); + useLines.push({ + type: "progress", + }); + } else if (line.startsWith(promptLiteralStart)) { + saveBuffer(); + const value = line.replace(promptLiteralStart, "").trimEnd(); + useLines.push({ + type: "input", + value: value, + }); + } else if (line.startsWith("// ")) { + saveBuffer(); + const value = "💬 " + line.replace("// ", "").trimEnd(); + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0, + }); + } else if (line.startsWith(customPromptLiteralStart)) { + saveBuffer(); + const promptStart = line.indexOf(promptLiteralStart); + if (promptStart === -1) { + console.error("Custom prompt found but no end delimiter", line); + } + const prompt = line + .slice(0, promptStart) + .replace(customPromptLiteralStart, ""); + let value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt, + }); + } else { + buffer.push(line); + } + } + saveBuffer(); + const div = document.createElement("div"); + node.replaceWith(div); + const termynal = new Termynal(div, { + lineData: useLines, + noInit: true, + lineDelay: 500, }); - } - window.addEventListener("scroll", loadVisibleTermynals); - createTermynals(); - loadVisibleTermynals(); + termynals.push(termynal); + }); + } + + function loadVisibleTermynals() { + termynals = termynals.filter((termynal) => { + if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { + termynal.init(); + return false; + } + return true; + }); + } + window.addEventListener("scroll", loadVisibleTermynals); + createTermynals(); + loadVisibleTermynals(); } function addCopyButtons() { - document.querySelectorAll('pre code').forEach(function (codeBlock) { - var button = document.createElement('button'); - button.className = 'copy-code-button'; - button.type = 'button'; - button.innerText = 'Copy'; - button.addEventListener('click', function () { - navigator.clipboard.writeText(codeBlock.innerText).then(function () { - /* clipboard successfully set */ - button.innerText = 'Copied!'; - setTimeout(function () { - button.innerText = 'Copy'; - }, 2000); - }, function () { - /* clipboard write failed */ - button.innerText = 'Failed to copy'; - }); - }); - - var pre = codeBlock.parentNode; - if (pre.parentNode.classList.contains('highlight')) { - var highlight = pre.parentNode; - highlight.parentNode.insertBefore(button, highlight); - } + document.querySelectorAll("pre code").forEach(function (codeBlock) { + var button = document.createElement("button"); + button.className = "copy-code-button"; + button.type = "button"; + button.innerText = "Copy"; + button.addEventListener("click", function () { + navigator.clipboard.writeText(codeBlock.innerText).then( + function () { + /* clipboard successfully set */ + button.innerText = "Copied!"; + setTimeout(function () { + button.innerText = "Copy"; + }, 2000); + }, + function () { + /* clipboard write failed */ + button.innerText = "Failed to copy"; + }, + ); }); + + var pre = codeBlock.parentNode; + if (pre.parentNode.classList.contains("highlight")) { + var highlight = pre.parentNode; + highlight.parentNode.insertBefore(button, highlight); + } + }); } // Call addCopyButtons in your main function or after the DOM content is fully loaded async function main() { - setupTermynal(); - addCopyButtons(); // Add this line to your existing main function + setupTermynal(); + addCopyButtons(); // Add this line to your existing main function } - -main() +main(); diff --git a/docs/js/termynal.js b/docs/js/termynal.js index 45bb371..6c54353 100644 --- a/docs/js/termynal.js +++ b/docs/js/termynal.js @@ -8,256 +8,275 @@ * @license MIT */ -'use strict'; +"use strict"; /** Generate a terminal widget. */ class Termynal { - /** - * Construct the widget's settings. - * @param {(string|Node)=} container - Query selector or container element. - * @param {Object=} options - Custom settings. - * @param {string} options.prefix - Prefix to use for data attributes. - * @param {number} options.startDelay - Delay before animation, in ms. - * @param {number} options.typeDelay - Delay between each typed character, in ms. - * @param {number} options.lineDelay - Delay between each line, in ms. - * @param {number} options.progressLength - Number of characters displayed as progress bar. - * @param {string} options.progressChar – Character to use for progress bar, defaults to █. - * @param {number} options.progressPercent - Max percent of progress. - * @param {string} options.cursor – Character to use for cursor, defaults to ▋. - * @param {Object[]} lineData - Dynamically loaded line data objects. - * @param {boolean} options.noInit - Don't initialise the animation. - */ - constructor(container = '#termynal', options = {}) { - this.container = (typeof container === 'string') ? document.querySelector(container) : container; - this.pfx = `data-${options.prefix || 'ty'}`; - this.originalStartDelay = this.startDelay = options.startDelay - || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; - this.originalTypeDelay = this.typeDelay = options.typeDelay - || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; - this.originalLineDelay = this.lineDelay = options.lineDelay - || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; - this.progressLength = options.progressLength - || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; - this.progressChar = options.progressChar - || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; - this.progressPercent = options.progressPercent - || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; - this.cursor = options.cursor - || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; - this.lineData = this.lineDataToElements(options.lineData || []); - this.loadLines() - if (!options.noInit) this.init() - } + /** + * Construct the widget's settings. + * @param {(string|Node)=} container - Query selector or container element. + * @param {Object=} options - Custom settings. + * @param {string} options.prefix - Prefix to use for data attributes. + * @param {number} options.startDelay - Delay before animation, in ms. + * @param {number} options.typeDelay - Delay between each typed character, in ms. + * @param {number} options.lineDelay - Delay between each line, in ms. + * @param {number} options.progressLength - Number of characters displayed as progress bar. + * @param {string} options.progressChar – Character to use for progress bar, defaults to █. + * @param {number} options.progressPercent - Max percent of progress. + * @param {string} options.cursor – Character to use for cursor, defaults to ▋. + * @param {Object[]} lineData - Dynamically loaded line data objects. + * @param {boolean} options.noInit - Don't initialise the animation. + */ + constructor(container = "#termynal", options = {}) { + this.container = + typeof container === "string" + ? document.querySelector(container) + : container; + this.pfx = `data-${options.prefix || "ty"}`; + this.originalStartDelay = this.startDelay = + options.startDelay || + parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || + 600; + this.originalTypeDelay = this.typeDelay = + options.typeDelay || + parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || + 90; + this.originalLineDelay = this.lineDelay = + options.lineDelay || + parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || + 1500; + this.progressLength = + options.progressLength || + parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || + 40; + this.progressChar = + options.progressChar || + this.container.getAttribute(`${this.pfx}-progressChar`) || + "█"; + this.progressPercent = + options.progressPercent || + parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || + 100; + this.cursor = + options.cursor || + this.container.getAttribute(`${this.pfx}-cursor`) || + "▋"; + this.lineData = this.lineDataToElements(options.lineData || []); + this.loadLines(); + if (!options.noInit) this.init(); + } - loadLines() { - // Load all the lines and create the container so that the size is fixed - // Otherwise it would be changing and the user viewport would be constantly - // moving as she/he scrolls - const finish = this.generateFinish() - finish.style.visibility = 'hidden' - this.container.appendChild(finish) - // Appends dynamically loaded lines to existing line elements. - this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); - for (let line of this.lines) { - line.style.visibility = 'hidden' - this.container.appendChild(line) - } - const restart = this.generateRestart() - restart.style.visibility = 'hidden' - this.container.appendChild(restart) - this.container.setAttribute('data-termynal', ''); + loadLines() { + // Load all the lines and create the container so that the size is fixed + // Otherwise it would be changing and the user viewport would be constantly + // moving as she/he scrolls + const finish = this.generateFinish(); + finish.style.visibility = "hidden"; + this.container.appendChild(finish); + // Appends dynamically loaded lines to existing line elements. + this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat( + this.lineData, + ); + for (let line of this.lines) { + line.style.visibility = "hidden"; + this.container.appendChild(line); } + const restart = this.generateRestart(); + restart.style.visibility = "hidden"; + this.container.appendChild(restart); + this.container.setAttribute("data-termynal", ""); + } + /** + * Initialise the widget, get lines, clear container and start animation. + */ + init() { /** - * Initialise the widget, get lines, clear container and start animation. + * Calculates width and height of Termynal container. + * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. */ - init() { - /** - * Calculates width and height of Termynal container. - * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. - */ - const containerStyle = getComputedStyle(this.container); - this.container.style.width = containerStyle.width !== '0px' ? - containerStyle.width : undefined; - this.container.style.minHeight = containerStyle.height !== '0px' ? - containerStyle.height : undefined; + const containerStyle = getComputedStyle(this.container); + this.container.style.width = + containerStyle.width !== "0px" ? containerStyle.width : undefined; + this.container.style.minHeight = + containerStyle.height !== "0px" ? containerStyle.height : undefined; - this.container.setAttribute('data-termynal', ''); - this.container.innerHTML = ''; - for (let line of this.lines) { - line.style.visibility = 'visible' - } - this.start(); + this.container.setAttribute("data-termynal", ""); + this.container.innerHTML = ""; + for (let line of this.lines) { + line.style.visibility = "visible"; } + this.start(); + } - /** - * Start the animation and rener the lines depending on their data attributes. - */ - async start() { - this.addFinish() - await this._wait(this.startDelay); - - for (let line of this.lines) { - const type = line.getAttribute(this.pfx); - const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; - - if (type == 'input') { - line.setAttribute(`${this.pfx}-cursor`, this.cursor); - await this.type(line); - await this._wait(delay); - } + /** + * Start the animation and rener the lines depending on their data attributes. + */ + async start() { + this.addFinish(); + await this._wait(this.startDelay); - else if (type == 'progress') { - await this.progress(line); - await this._wait(delay); - } + for (let line of this.lines) { + const type = line.getAttribute(this.pfx); + const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; - else { - this.container.appendChild(line); - await this._wait(delay); - } + if (type == "input") { + line.setAttribute(`${this.pfx}-cursor`, this.cursor); + await this.type(line); + await this._wait(delay); + } else if (type == "progress") { + await this.progress(line); + await this._wait(delay); + } else { + this.container.appendChild(line); + await this._wait(delay); + } - line.removeAttribute(`${this.pfx}-cursor`); - } - this.addRestart() - this.finishElement.style.visibility = 'hidden' - this.lineDelay = this.originalLineDelay - this.typeDelay = this.originalTypeDelay - this.startDelay = this.originalStartDelay + line.removeAttribute(`${this.pfx}-cursor`); } + this.addRestart(); + this.finishElement.style.visibility = "hidden"; + this.lineDelay = this.originalLineDelay; + this.typeDelay = this.originalTypeDelay; + this.startDelay = this.originalStartDelay; + } - generateRestart() { - const restart = document.createElement('a') - restart.onclick = (e) => { - e.preventDefault() - this.container.innerHTML = '' - this.init() - } - restart.href = '#' - restart.setAttribute('data-terminal-control', '') - restart.innerHTML = "restart ↻" - return restart - } + generateRestart() { + const restart = document.createElement("a"); + restart.onclick = (e) => { + e.preventDefault(); + this.container.innerHTML = ""; + this.init(); + }; + restart.href = "#"; + restart.setAttribute("data-terminal-control", ""); + restart.innerHTML = "restart ↻"; + return restart; + } - generateFinish() { - const finish = document.createElement('a') - finish.onclick = (e) => { - e.preventDefault() - this.lineDelay = 0 - this.typeDelay = 0 - this.startDelay = 0 - } - finish.href = '#' - finish.setAttribute('data-terminal-control', '') - finish.innerHTML = "fast →" - this.finishElement = finish - return finish - } + generateFinish() { + const finish = document.createElement("a"); + finish.onclick = (e) => { + e.preventDefault(); + this.lineDelay = 0; + this.typeDelay = 0; + this.startDelay = 0; + }; + finish.href = "#"; + finish.setAttribute("data-terminal-control", ""); + finish.innerHTML = "fast →"; + this.finishElement = finish; + return finish; + } - addRestart() { - const restart = this.generateRestart() - this.container.appendChild(restart) - } + addRestart() { + const restart = this.generateRestart(); + this.container.appendChild(restart); + } - addFinish() { - const finish = this.generateFinish() - this.container.appendChild(finish) - } + addFinish() { + const finish = this.generateFinish(); + this.container.appendChild(finish); + } - /** - * Animate a typed line. - * @param {Node} line - The line element to render. - */ - async type(line) { - const chars = [...line.textContent]; - line.textContent = ''; - this.container.appendChild(line); + /** + * Animate a typed line. + * @param {Node} line - The line element to render. + */ + async type(line) { + const chars = [...line.textContent]; + line.textContent = ""; + this.container.appendChild(line); - for (let char of chars) { - const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; - await this._wait(delay); - line.textContent += char; - } + for (let char of chars) { + const delay = + line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; + await this._wait(delay); + line.textContent += char; } + } - /** - * Animate a progress bar. - * @param {Node} line - The line element to render. - */ - async progress(line) { - const progressLength = line.getAttribute(`${this.pfx}-progressLength`) - || this.progressLength; - const progressChar = line.getAttribute(`${this.pfx}-progressChar`) - || this.progressChar; - const chars = progressChar.repeat(progressLength); - const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) - || this.progressPercent; - line.textContent = ''; - this.container.appendChild(line); + /** + * Animate a progress bar. + * @param {Node} line - The line element to render. + */ + async progress(line) { + const progressLength = + line.getAttribute(`${this.pfx}-progressLength`) || this.progressLength; + const progressChar = + line.getAttribute(`${this.pfx}-progressChar`) || this.progressChar; + const chars = progressChar.repeat(progressLength); + const progressPercent = + line.getAttribute(`${this.pfx}-progressPercent`) || this.progressPercent; + line.textContent = ""; + this.container.appendChild(line); - for (let i = 1; i < chars.length + 1; i++) { - await this._wait(this.typeDelay); - const percent = Math.round(i / chars.length * 100); - line.textContent = `${chars.slice(0, i)} ${percent}%`; - if (percent>progressPercent) { - break; - } - } + for (let i = 1; i < chars.length + 1; i++) { + await this._wait(this.typeDelay); + const percent = Math.round((i / chars.length) * 100); + line.textContent = `${chars.slice(0, i)} ${percent}%`; + if (percent > progressPercent) { + break; + } } + } - /** - * Helper function for animation delays, called with `await`. - * @param {number} time - Timeout, in ms. - */ - _wait(time) { - return new Promise(resolve => setTimeout(resolve, time)); - } + /** + * Helper function for animation delays, called with `await`. + * @param {number} time - Timeout, in ms. + */ + _wait(time) { + return new Promise((resolve) => setTimeout(resolve, time)); + } - /** - * Converts line data objects into line elements. - * - * @param {Object[]} lineData - Dynamically loaded lines. - * @param {Object} line - Line data object. - * @returns {Element[]} - Array of line elements. - */ - lineDataToElements(lineData) { - return lineData.map(line => { - let div = document.createElement('div'); - div.innerHTML = `${line.value || ''}`; + /** + * Converts line data objects into line elements. + * + * @param {Object[]} lineData - Dynamically loaded lines. + * @param {Object} line - Line data object. + * @returns {Element[]} - Array of line elements. + */ + lineDataToElements(lineData) { + return lineData.map((line) => { + let div = document.createElement("div"); + div.innerHTML = `${ + line.value || "" + }`; - return div.firstElementChild; - }); - } + return div.firstElementChild; + }); + } - /** - * Helper function for generating attributes string. - * - * @param {Object} line - Line data object. - * @returns {string} - String of attributes. - */ - _attributes(line) { - let attrs = ''; - for (let prop in line) { - // Custom add class - if (prop === 'class') { - attrs += ` class=${line[prop]} ` - continue - } - if (prop === 'type') { - attrs += `${this.pfx}="${line[prop]}" ` - } else if (prop !== 'value') { - attrs += `${this.pfx}-${prop}="${line[prop]}" ` - } - } - return attrs; + /** + * Helper function for generating attributes string. + * + * @param {Object} line - Line data object. + * @returns {string} - String of attributes. + */ + _attributes(line) { + let attrs = ""; + for (let prop in line) { + // Custom add class + if (prop === "class") { + attrs += ` class=${line[prop]} `; + continue; + } + if (prop === "type") { + attrs += `${this.pfx}="${line[prop]}" `; + } else if (prop !== "value") { + attrs += `${this.pfx}-${prop}="${line[prop]}" `; + } } + return attrs; + } } /** -* HTML API: If current script has container(s) specified, initialise Termynal. -*/ -if (document.currentScript.hasAttribute('data-termynal-container')) { - const containers = document.currentScript.getAttribute('data-termynal-container'); - containers.split('|') - .forEach(container => new Termynal(container)) + * HTML API: If current script has container(s) specified, initialise Termynal. + */ +if (document.currentScript.hasAttribute("data-termynal-container")) { + const containers = document.currentScript.getAttribute( + "data-termynal-container", + ); + containers.split("|").forEach((container) => new Termynal(container)); } From c00b031a9f6bdd1e6f01ed62faeec560e36627d3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 13:45:07 +0100 Subject: [PATCH 372/451] =?UTF-8?q?=F0=9F=93=9D=20Correct=20code=20block?= =?UTF-8?q?=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index 5d50e65..2d7e897 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -68,7 +68,7 @@ print(recipe.ingredients) instructions: list[str] duration: int -``` + ``` A Recipe class is defined, inheriting from BaseModel (pydantic library). This class specifies the structure of the output data, which you can customize. From 3a1d4e748124d27891fefb17f8b97c98468a37d7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 13:45:07 +0100 Subject: [PATCH 373/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20mkdocs=20naviga?= =?UTF-8?q?tion=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index b922b3c..c9dc6b3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,12 @@ nav: - "Enums": "features/enums.md" - "Error Output": "features/error_output.md" - "Literals": "features/literals.md" + - "Structured vision output": "features/vision.md" + - "Streaming Output": "features/stream.md" + - "Ollama (And other models)": "features/ollama.md" + - "Static Router": "features/static_router.md" + - "Pydantic Models": "features/pydantic_validation.md" + - "OpenAI JSON Output": "features/openai_json_mode.md" - "Advanced": - "Async": "advanced/async.md" - "Signature": "advanced/signature.md" @@ -39,6 +45,7 @@ nav: - "Codebase Scaling": "advanced/codebase-scaling.md" - "Customization": "advanced/customization.md" - "Stream Parsing": "advanced/stream-parsing.md" + - "Custom Parsers": "advanced/custom-parser-types.md" - "Contributing": - "Contributing": "contributing/dev-setup.md" - "Codebase Structure": "contributing/codebase-structure.md" @@ -48,19 +55,7 @@ nav: - "Roadmap": "contributing/roadmap.md" - "License": "contributing/license.md" - "Changelog": "changelog.md" - - "Examples": - - "ChatGPT": "examples/chat.md" - - "Structured vision output": "examples/vision.md" - - "Streaming Output": "examples/stream.md" - - "Ollama (And other models)": "examples/ollama.md" - - "Dynamic Router": "examples/dynamic_router.md" - - "Static Router": "examples/static_router.md" - - "Enums": "examples/enums.md" - - "Error Output": "examples/error_output.md" - - "Literals": "examples/literals.md" - - "Pydantic Models": "examples/pydantic_validation.md" - - "OpenAI JSON Output": "examples/openai_json_mode.md" - - "API Reference": "api.md" +# - "API Reference": "api.md" theme: name: material From e58b495633329af955ae245e6c3979dd4dfb5513 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 13:54:30 +0100 Subject: [PATCH 374/451] =?UTF-8?q?=F0=9F=93=9D=20markdownlint-disable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/code-of-conduct.md | 2 +- docs/features/chat.md | 1 + docs/features/dynamic_router.md | 13 ++++----- docs/features/enums.md | 3 +- docs/features/error_output.md | 1 + docs/features/literals.md | 1 + docs/features/ollama.md | 2 ++ docs/features/openai_json_mode.md | 1 + docs/features/pydantic_validation.md | 20 ++++++++------ docs/features/static_router.md | 1 + docs/features/stream.md | 1 + docs/features/vision.md | 1 + docs/getting-started/installation.md | 1 + docs/getting-started/introduction.md | 41 ++++++++++------------------ docs/index.md | 1 + 15 files changed, 44 insertions(+), 46 deletions(-) diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md index b046dc9..148c2a8 100644 --- a/docs/contributing/code-of-conduct.md +++ b/docs/contributing/code-of-conduct.md @@ -40,4 +40,4 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org). diff --git a/docs/features/chat.md b/docs/features/chat.md index e4228d3..8eb3ad6 100644 --- a/docs/features/chat.md +++ b/docs/features/chat.md @@ -1,3 +1,4 @@ + # ChatGPT rebuild with memory/history !!! Example diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md index 27f1cce..bb798a5 100644 --- a/docs/features/dynamic_router.md +++ b/docs/features/dynamic_router.md @@ -1,3 +1,4 @@ + # Dynamic Chat Router with Funcchain !!! Example @@ -19,8 +20,6 @@ from typing import Any, Callable, TypedDict from funcchain.syntax.executable import compile_runnable from pydantic import BaseModel, Field -# Dynamic Router Definition: - class Route(TypedDict): handler: Callable @@ -63,9 +62,6 @@ class DynamicChatRouter(BaseModel): return self.routes[selected_route]["handler"](user_query, **kwargs) -# Example Usage: - - def handle_pdf_requests(user_query: str) -> str: return "Handling PDF requests with user query: " + user_query @@ -99,11 +95,11 @@ router = DynamicChatRouter( router.invoke_route("Can you summarize this csv?") ``` -Demo +## Demo
```python -User: +Input: $ Can you summarize this csv? $ ............... Handling CSV requests with user query: Can you summarize this csv? @@ -115,7 +111,8 @@ Handling CSV requests with user query: Can you summarize this csv? !!! Step-by-Step **Nececary imports** - ``` + + ```python from enum import Enum from typing import Any, Callable, TypedDict diff --git a/docs/features/enums.md b/docs/features/enums.md index 3cfe7ab..8086942 100644 --- a/docs/features/enums.md +++ b/docs/features/enums.md @@ -1,4 +1,5 @@ -## Decision Making with Enums and Funcchain + +# Decision Making with Enums and Funcchain !!! Example See [enums.py](https://github.com/shroominic/funcchain/blob/main/examples/enums.py) diff --git a/docs/features/error_output.md b/docs/features/error_output.md index 7fcfcf5..0dccf55 100644 --- a/docs/features/error_output.md +++ b/docs/features/error_output.md @@ -1,3 +1,4 @@ + # Example of raising an error !!! Example diff --git a/docs/features/literals.md b/docs/features/literals.md index 2d6d0c5..ee9b26d 100644 --- a/docs/features/literals.md +++ b/docs/features/literals.md @@ -1,3 +1,4 @@ + # Literal Type Enforcement in Funcchain !!! Example diff --git a/docs/features/ollama.md b/docs/features/ollama.md index fda60bb..b433c76 100644 --- a/docs/features/ollama.md +++ b/docs/features/ollama.md @@ -1,3 +1,4 @@ + # Different LLMs with funcchain EASY TO USE !!! Example @@ -57,6 +58,7 @@ if __name__ == "__main__":
## Instructions + !!! Step-by-Step **Necessary Imports** diff --git a/docs/features/openai_json_mode.md b/docs/features/openai_json_mode.md index ec5f769..c2bcd38 100644 --- a/docs/features/openai_json_mode.md +++ b/docs/features/openai_json_mode.md @@ -1,3 +1,4 @@ + # JSON structured Output using Funcchain with OenAI !!! Example diff --git a/docs/features/pydantic_validation.md b/docs/features/pydantic_validation.md index 7182d46..f1d2458 100644 --- a/docs/features/pydantic_validation.md +++ b/docs/features/pydantic_validation.md @@ -1,3 +1,4 @@ + # Task Creation with Validated Fields !!! Example @@ -51,22 +52,23 @@ if __name__ == "__main__": Demo
-```python -User: -$ cleanup the kitchen + ```python + User: + $ cleanup the kitchen -task=Task -name='cleanup', -difficulty=30, -keywords=['kitchen', 'cleanup'] + task=Task + name='cleanup', + difficulty=30, + keywords=['kitchen', 'cleanup'] + ``` -```
-##Instructions +## Instructions !!! Step-by-Step **Necessary Imports** + ```python from funcchain import chain, settings from pydantic import BaseModel, field_validator diff --git a/docs/features/static_router.md b/docs/features/static_router.md index 9b44955..753feef 100644 --- a/docs/features/static_router.md +++ b/docs/features/static_router.md @@ -1,3 +1,4 @@ + # Static Routing with Funcchain and Pydantic !!! Example diff --git a/docs/features/stream.md b/docs/features/stream.md index d93b390..a9b3412 100644 --- a/docs/features/stream.md +++ b/docs/features/stream.md @@ -1,3 +1,4 @@ + # Streaming with Funcchain !!! Example diff --git a/docs/features/vision.md b/docs/features/vision.md index e146a64..323b218 100644 --- a/docs/features/vision.md +++ b/docs/features/vision.md @@ -1,3 +1,4 @@ + # Image Analysis with Funcchain and Pydantic !!! Example diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4444556..d6c3b9f 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,3 +1,4 @@ + # Installation
diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md index 2d7e897..5e52abc 100644 --- a/docs/getting-started/introduction.md +++ b/docs/getting-started/introduction.md @@ -1,3 +1,6 @@ + +# Introduction + [![Version](https://badge.fury.io/py/funcchain.svg)](https://badge.fury.io/py/funcchain) [![tests](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml) ![PyVersion](https://img.shields.io/pypi/pyversions/funcchain) @@ -14,21 +17,6 @@
-!!! Useful: Langsmith integration - - Add those lines to .env and funcchain will use langsmith tracing. - - ```bash - LANGCHAIN_TRACING_V2=true - LANGCHAIN_API_KEY="ls__api_key" - LANGCHAIN_PROJECT="PROJECT_NAME" - ``` - - Langsmith is used to understand what happens under the hood of your LLM generations. - When multiple LLM calls are used for an output they can be logged for debugging. - -## Introduction - `funcchain` is the _most pythonic_ way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. It works perfect with OpenAI Functions and soon with other models using JSONFormer. @@ -340,8 +328,6 @@ for obj in result.objects: Its important that the fields defined earlier are mentioned here with the prompt `Analyse the image and extract its`... -## Demo -
``` print(analyse_image(image: Image.Image)) @@ -395,8 +381,7 @@ print(poem.analysis) ```python from pydantic import BaseModel, Field from funcchain import chain, settings - -``` + ``` **Choose and enjoy** ```python @@ -435,18 +420,20 @@ print(poem.analysis) print(poem.analysis) ``` -# Demo - -
- ``` - poem = analyze("I really like when my dog does a trick!") +
- $ .................. +!!! Useful: Langsmith integration - Add demo + Add those lines to .env and funcchain will use langsmith tracing. + ```bash + LANGCHAIN_TRACING_V2=true + LANGCHAIN_API_KEY="ls__api_key" + LANGCHAIN_PROJECT="PROJECT_NAME" ``` -
+ + Langsmith is used to understand what happens under the hood of your LLM generations. + When multiple LLM calls are used for an output they can be logged for debugging. ## Features diff --git a/docs/index.md b/docs/index.md index 8408aa7..7a289dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,4 @@ + # Introduction [![Version](https://badge.fury.io/py/funcchain.svg)](https://badge.fury.io/py/funcchain) From 992e4df86f6ec0da6297ab49c762f1de7bcb56e5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:10:35 +0100 Subject: [PATCH 375/451] =?UTF-8?q?=E2=9C=A8=20Update=20chain=20documentat?= =?UTF-8?q?ion=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/chain.md | 54 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/concepts/chain.md b/docs/concepts/chain.md index 3a7b7dd..9c0b87c 100644 --- a/docs/concepts/chain.md +++ b/docs/concepts/chain.md @@ -1,10 +1,56 @@ -# chain() +# Chain + +## chain( ) method explain about chain like in usage.md and show achain and @runnable exalain a bit how chain works under the hood -## Usage +```python +from funcchain import chain + +def ask(question: str) -> str: + """ + Answer the given question. + """ + return chain() + +ask("What is the capital of Germany?") +# => "The capital of Germany is Berlin." +``` + +## achain( ) method + +Async version of the chain() method. + +```python +import asyncio +from funcchain import achain + +async def ask(question: str) -> str: + """ + Answer the given question. + """ + return await achain() + +asyncio.run(ask("What is the capital of Germany?")) +# => "The capital of Germany is Berlin." +``` + +## runnable decorator + +The `@runnable` decorator is used to compile a chain function into a langchain runnable object. +You just write a normal funcchain function using chain() and then decorate it with @runnable. + +```python +from funcchain import chain, runnable -## achain() +@runnable +def ask(question: str) -> str: + """ + Answer the given question. + """ + return chain() -## @runnable +ask.invoke(input={"question": "What is the capital of Germany?"}) +# => "The capital of Germany is Berlin." +``` From c311f6f407b580f25e6a81721ea406a06ac3c310 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:10:35 +0100 Subject: [PATCH 376/451] =?UTF-8?q?=E2=9C=A8=20Add=20Error=20handling=20ex?= =?UTF-8?q?ample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/errors.md | 49 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/concepts/errors.md b/docs/concepts/errors.md index 4dc82ab..d6ac68a 100644 --- a/docs/concepts/errors.md +++ b/docs/concepts/errors.md @@ -1,5 +1,52 @@ # Errors +## Example + +```python +from funcchain import BaseModel, Error, chain +from rich import print + + +class User(BaseModel): + name: str + email: str | None + + +def extract_user_info(text: str) -> User | Error: + """ + Extract the user information from the given text. + In case you do not have enough infos, return an error. + """ + return chain() + +print(extract_user_info("hey what's up?")) +# => Error(title='Invalid Input', description='The input text does not contain user information.') + +print(extract_user_info("I'm John and my email is john@mail.com")) +# => User(name='John', email='john@mail.com') +``` + ## Error Type -(currently only supported for union output types (e.g. `Answer | Error`) so only openai models) +(currently only supported for union output types e.g. `Answer | Error` so only openai models) + +The Error type is a special type that can be used to return an error from a chain function. +It is just a pydantic model with a title and description field. + +```python +class Error(BaseModel): + """ + Fallback function for invalid input. + If you are unsure on what function to call, use this error function as fallback. + This will tell the user that the input is not valid. + """ + + title: str = Field(description="CamelCase Name titeling the error") + description: str = Field(..., description="Short description of the unexpected situation") + + def __raise__(self) -> None: + raise Exception(self.description) +``` + +You can also create your own error types by inheriting from the Error type. +Or just do it similar to the example above. From 8d0f6bb75df4d299a4f5e1dcd6ae8603526103ec Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:10:35 +0100 Subject: [PATCH 377/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20input=20handlin?= =?UTF-8?q?g=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/input.md | 100 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/docs/concepts/input.md b/docs/concepts/input.md index 9b55c58..39175b4 100644 --- a/docs/concepts/input.md +++ b/docs/concepts/input.md @@ -1,11 +1,107 @@ # Input Arguments -## String Inputs +Funcchain utilises your function's input arguments including type hints to compile your prompt. +You can utilise the following types: + +## Strings + +All string inputs serve as classic prompt placeholders and are replaced with the input value. +You can insert anything as long as you cast it to a string and the language model will see its as text. + +```python +def create_username(full_name: str, email: str) -> str: + """ + Create a creative username from the given full name and email. + """ + return chain() +``` + +All strings that are not mentioned in the instructions are automatically added to the beginning of the prompt. + +When calling `create_username("John Doe", "john.doe@gmail.com")` the compiled prompt will look like this: + +```html + + FULL_NAME: + John Doe + + EMAIL: + john.doe@gmail.com + + Create a creative username from the given full name and email. + +``` + +The language model will then be able to use the input values to generate a good username. + +You can also manually format your instructions if you want to have more control over the prompt. +Use jinja2 syntax to insert the input values. + +```python +def create_username(full_name: str, email: str) -> str: + """ + Create a creative username for {{ full_name }} with the mail {{ email }}. + """ + return chain() +``` + +Compiles to: + +```html + + Create a creative username for John Doe with the mail john.doe@gmail.com. + +``` ## Pydantic Models +You can also use pydantic models as input arguments. +This is useful if you already have complex data structures that you want to use as input. + +```python +class User(BaseModel): + full_name: str + email: str + +def create_username(user: User) -> str: + """ + Create a creative username from the given user. + """ + return chain() +``` + +By default, the pydantic model is converted to a string using the `__str__` method +and then added to the prompt. + +```html + + USER: + full_name='Herbert Geier' email='hello@bert.com' + + Create a creative username from the given user. + +``` + +If you want more control you can override the `__str__` method of your pydantic model. +Or use jinja2 syntax to manually unpack the model. + +```python +class User(BaseModel): + full_name: str + email: str + +def create_username(user: User) -> str: + """ + Create a creative username for {{ user.full_name }} with the mail {{ user.email }}. + """ + return chain() +``` + ## Other Types +More special types are coming soon. + ## Important Notes -always use type hints +You need to use type hints for all your input arguments. +Otherwise, funcchain will just ignore them. From f995e3be0be3a40b25f67520022e61f481c0862c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:10:35 +0100 Subject: [PATCH 378/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20concepts=20docu?= =?UTF-8?q?mentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/overview.md | 130 +++++--------------------------------- 1 file changed, 15 insertions(+), 115 deletions(-) diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index 7d32816..d168e8e 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -1,117 +1,17 @@ # Concepts -TODO: rewrite this - -## Concepts Overview - -| name | description | -| --------- | -------------------------------------------------- | -| chain | Main funcchain to get responses from the assistant | -| achain | Async version of chain | -| settings | Global settings object | -| BaseModel | Pydantic model base class | - -## chain - -The `chain` function is the main interface to get responses from the assistant. It handles creating the prompt, querying the model, and parsing the response. - -Key things: - -- Takes instruction and system prompt strings to create the prompt -- Automatically extracts docstring of calling function as instruction -- Gets output parser based on return type annotation -- Supports OpenAI Functions under the hood -- Retries on parser failure -- Logs tokens usage - -Usage: - -```python -from funcchain import chain - - -def get_weather(city: str) -> str: - """ - Get the weather for {city}. - """ - return chain() - -print(get_weather("Barcelona")) -``` - -## achain - -The `achain` function is an async version of `chain` that can be awaited. - -Usage: - -```python -from funcchain import achain -import asyncio - - -async def get_weather(city: str) -> str: - """ - Get the weather for {city}. - """ - return await achain() - - -print(asyncio.run(get_weather("Barcelona"))) -``` - -## settings - -The `settings` object contains global settings for funcchain. - -Key attributes: - -- `llm`: Configures the default llm -- `max_tokens`: Max tokens per request -- `default_system_prompt`: Default system prompt -- `openai_api_key`: OpenAI API key -- `model_kwargs()`: kwargs for model like temperature - -Usage: - -```python -from funcchain import settings - -settings.llm = MyCustomChatModel() -settings.max_tokens = 2048 -``` - -## BaseModel - -`BaseModel` is the Pydantic model base class used to define output schemas. - -Funcchain can automatically parse responses to Pydantic models. - -Usage: - -```python -from funcchain import chain -from pydantic import BaseModel, Field - - -class Article(BaseModel): - title: str = Field(description="Title of the article") - description: str = Field(description="Description of the content of the article") - - -def summarize(text: str) -> Article: - """ - Summarize the text into an Article: - {text} - """ - return chain() - - -print( - summarize( - """ - AI has the potential to revolutionize education, offering personalized and individualized teaching, and improved learning outcomes. AI can analyze student data and provide real-time feedback to teachers and students, allowing them to adjust their teaching and learning strategies accordingly. One of the biggest benefits of AI in education is the ability to provide personalized and individualized teaching. AI can analyze student data and create a personalized learning plan for each individual student, taking into account their strengths, weaknesses, and learning styles. This approach has the potential to dramatically improve learning outcomes and engagement. The potential of AI in education is enormous, and it is expected to revolutionize the way we approach degree and diploma programs in the future. AI-powered technologies can provide students with real-time feedback, help them to stay on track with their studies, and offer a more personalized and engaging learning experience. - """ - ) -) -``` +## Overview + +| name | description | +| ------------------------------------------------------------------- | ---------------------------------------------------- | +| [chain()](chain.md) | Core funcchain syntax component to write chains | +| [Input Args](input.md) | prompt placeholders and input for your chains | +| [BaseModel](pydantic.md) | Core component to create pydantic models/classes | +| [Settings](../getting-started/config.md#set-global-settings) | Global settings object with for all your chains | +| [SettingsOverride](../getting-started/config.md#set-local-settings) | Local settings dict for a specific chain | +| [OutputParser](parser.md) | Parses the llm output into your desired shape | +| [Prompting](prompting.md) | Templating system and techniques for writing prompts | +| [Vision](vision.md) | LLM that can also takes images as input/context | +| [Streaming](streaming.md) | Token by token streaming of llm output | +| [Unions](unions.md) | Pydantic union types for your models | +| [LangChain](langchain.md) | Library for building cognitive systems | From 86c2692a88293b7d963c4216bf9b61c71003e2ba Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:10:35 +0100 Subject: [PATCH 379/451] =?UTF-8?q?=F0=9F=93=9D=20Rename=20demos.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/demos.md | 388 ++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 docs/getting-started/demos.md diff --git a/docs/getting-started/demos.md b/docs/getting-started/demos.md new file mode 100644 index 0000000..37ebd15 --- /dev/null +++ b/docs/getting-started/demos.md @@ -0,0 +1,388 @@ + +# Demos + +## Simple Structured Output + +```python +from funcchain import chain +from pydantic import BaseModel + +# define your output shape +class Recipe(BaseModel): + ingredients: list[str] + instructions: list[str] + duration: int + +# write prompts utilising all native python features +def generate_recipe(topic: str) -> Recipe: + """ + Generate a recipe for a given topic. + """ + return chain() # <- this is doing all the magic + +# generate llm response +recipe = generate_recipe("christmas dinner") + +# recipe is automatically converted as pydantic model +print(recipe.ingredients) +``` + +!!! Step-by-Step + + ```python + # define your output shape + class Recipe(BaseModel): + ingredients: list[str] + instructions: list[str] + duration: int + + ``` + + A Recipe class is defined, inheriting from BaseModel (pydantic library). This class + specifies the structure of the output data, which you can customize. + In the example it includes a list of ingredients, a list of instructions, and an integer + representing the duration + + ```python + # write prompts utilising all native python features + def generate_recipe(topic: str) -> Recipe: + """ + Generate a recipe for a given topic. + """ + return chain() # <- this is doing all the magic + ``` + In this example the `generate_recipe` function takes a topic string and returns a `Recipe` instance for that topic. + # Understanding chain() Functionality + Chain() is the backend magic of funcchain. Behind the szenes it creates the llm function from the function signature and docstring. + Meaning it will turn your function into usable LLM input. + + The `chain()` function is the core component of funcchain. It takes the docstring, input arguments and return type of the function and compiles everything into a langchain runnable . It then executes the prompt with your input arguments if you call the function and returns the parsed result. + + # Get your response + ```python + # generate llm response + recipe = generate_recipe("christmas dinner") + + # recipe is automatically converted as pydantic model + print(recipe.ingredients) + ``` + +### Demo + +
+ ``` + $ print(generate_recipe("christmas dinner").ingredients + + ['turkey', 'potatoes', 'carrots', 'brussels sprouts', 'cranberry sauce', 'gravy','butter', 'salt', 'pepper', 'rosemary'] + + ``` +
+ +## Complex Structured Output + +([full code](../index.md#complex-example)) + +!!! Step-by-Step + **Nececary Imports** + + ```python + from pydantic import BaseModel, Field + from funcchain import chain + ``` + + **Data Structures and Model Definitions** + ```python + # define nested models + class Item(BaseModel): + name: str = Field(description="Name of the item") + description: str = Field(description="Description of the item") + keywords: list[str] = Field(description="Keywords for the item") + + class ShoppingList(BaseModel): + items: list[Item] + store: str = Field(description="The store to buy the items from") + + class TodoList(BaseModel): + todos: list[Item] + urgency: int = Field(description="The urgency of all tasks (1-10)") + + ``` + + In this example, we create a more complex data structure with nested models. + The Item model defines the attributes of a single item, such as its name, description, and keywords. + ShoppingList and TodoList models define the attributes of a shopping list and a todo list, utilizing the Item model as a nested model. + + You can define new Pydantic models or extend existing ones by adding additional fields or methods. The general approach is to identify the data attributes relevant to your application and create corresponding model classes with these attributes. + + The Field descriptions serve as prompts for the language model to understand the data structure. + Additionally you can include a docstring for each model class to provide further information to the LLM. + + !!! Important + Everything including class names, argument names, doc string and field descriptions are part of the prompt and can be optimised using prompting techniques. + + + **Union types** + ```python + # support for union types + def extract_list(user_input: str) -> TodoList | ShoppingList: + """ + The user input is either a shopping List or a todo list. + """ + return chain() + ``` + The extract_list function uses the chain function to analyze user input and return a structured list: + In the example: + - Union Types: It can return either a TodoList or a ShoppingList, depending on the input. + - Usage of chain: chain simplifies the process, deciding the type of list to return. + + For your application this is going to serve as a router to route between your previously defined models. + + **Get a list from the user** (here as "lst") + ```python + # the model will choose the output type automatically + lst = extract_list( + input("Enter your list: ") + ) + + ``` + + **Define your custom handlers** + + And now its time to define what happens with the result. + You can then use the lst (list) variable to access the attributes of the list. + It utilizes pattern matching to determine the type of list and print the corresponding output. + + ```python + # custom handler based on type + match lst: + case ShoppingList(items=items, store=store): + print("Here is your Shopping List: ") + for item in items: + print(f"{item.name}: {item.description}") + print(f"You need to go to: {store}") + + case TodoList(todos=todos, urgency=urgency): + print("Here is your Todo List: ") + for item in todos: + print(f"{item.name}: {item.description}") + print(f"Urgency: {urgency}") + + ``` + +
+ ``` + lst = extract_list( + input("Enter your list: ") + ) + + User: + $ Complete project report, Prepare for meeting, Respond to emails; + $ if I don't respond I will be fired + + Output: + $ ............... + Here is your Todo List: + Complete your buisness tasks: project report, Prepare for meeting, Respond to emails + Urgency: 10 + //add real output + ``` +
+ +## Vision Models + +```python +from PIL import Image +from pydantic import BaseModel, Field +from funcchain import chain, settings + +# set global llm using model identifiers (see MODELS.md) +settings.llm = "openai/gpt-4-vision-preview" + +# everything defined is part of the prompt +class AnalysisResult(BaseModel): + """The result of an image analysis.""" + + theme: str = Field(description="The theme of the image") + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + +# easy use of images as input with structured output +def analyse_image(image: Image.Image) -> AnalysisResult: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + +result = analyse_image(Image.open("examples/assets/old_chinese_temple.jpg")) + +print("Theme:", result.theme) +print("Description:", result.description) +for obj in result.objects: + print("Found this object:", obj) +``` + +!!! Step-by-Step + **Nececary Imports** + + ```python + from PIL import Image + from pydantic import BaseModel, Field + from funcchain import chain, settings + ``` + + **Define Model** + set global llm using model identifiers see [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) + ```python + settings.llm = "openai/gpt-4-vision-preview" + ``` + Funcchains modularity allows for all kinds of models including local models + + + **Analize Image** + Get structured output from an image in our example `theme`, `description` and `objects` + ```python + # everything defined is part of the prompt + class AnalysisResult(BaseModel): + """The result of an image analysis.""" + + theme: str = Field(description="The theme of the image") + description: str = Field(description="A description of the image") + objects: list[str] = Field(description="A list of objects found in the image") + ``` + Adjsut the fields as needed. Play around with the example, feel free to experiment. + You can customize the analysis by modifying the fields of the `AnalysisResult` model. + + **Function to start the analysis** + + ```python + # easy use of images as input with structured output + def analyse_image(image: Image.Image) -> AnalysisResult: + """ + Analyse the image and extract its + theme, description and objects. + """ + return chain() + ``` + Chain() will handle the image input. + We here define again the fields from before `theme`, `description` and `objects` + + give an image as input `image: Image.Image` + + Its important that the fields defined earlier are mentioned here with the prompt + `Analyse the image and extract its`... + +
+ ``` + result = analyse_image( + Image.from_file("examples/assets/old_chinese_temple.jpg") + ) + + print("Theme:", result.theme) + print("Description:", result.description) + for obj in result.objects: + print("Found this object:", obj) + + $ .................. + + Theme: Traditional Japanese architecture and nature during rainfall + Description: The image depicts a serene rainy scene at night in a traditional Japanese setting. A two-story wooden building with glowing green lanterns is the focal point, surrounded by a cobblestone path, a blooming pink cherry blossom tree, and a stone lantern partially obscured by the rain. The atmosphere is tranquil and slightly mysterious. + Found this object: building + Found this object: green lanterns + Found this object: cherry blossom tree + Found this object: rain + Found this object: cobblestone path + Found this object: stone lantern + Found this object: wooden structure + + ``` + +
+ +## Seamless local model support + +Yes you can use funcchain without internet connection. +Start heating up your device. + +```python +from pydantic import BaseModel, Field +from funcchain import chain, settings + +# auto-download the model from huggingface +settings.llm = "ollama/openchat" + +class SentimentAnalysis(BaseModel): + analysis: str + sentiment: bool = Field(description="True for Happy, False for Sad") + +def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + +# generates using the local model +poem = analyze("I really like when my dog does a trick!") + +# promised structured output (for local models!) +print(poem.analysis) +``` + +!!! Step-by-Step + **Nececary Imports** + + ```python + from pydantic import BaseModel, Field + from funcchain import chain, settings + ``` + + **Choose and enjoy** + ```python + # auto-download the model from huggingface + settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" + ``` + + **Structured output definition** + With an input `str` a description can be added to return a boolean `true` or `false` + ```python + class SentimentAnalysis(BaseModel): + analysis: str + sentiment: bool = Field(description="True for Happy, False for Sad") + ``` + Experiment yourself by adding different descriptions for the true and false case. + + **Use `chain()` to analize** + Defines with natural language the analysis + ```python + def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + ``` + For your own usage adjust the str. Be precise and reference your classes again. + + **Generate and print the output** + ```python + **Use the analyze function and print output** + + # generates using the local model + poem = analyze("I really like when my dog does a trick!") + + # promised structured output (for local models!) + print(poem.analysis) + ``` + +!!! Useful + + For seeing whats going on inside the LLM you should try the Langsmith integration: + Add those lines to .env and funcchain will use langsmith tracing. + + ```bash + LANGCHAIN_TRACING_V2=true + LANGCHAIN_API_KEY="ls__api_key" + LANGCHAIN_PROJECT="PROJECT_NAME" + ``` + + Langsmith is used to understand what happens under the hood of your LLM generations. + When multiple LLM calls are used for an output they can be logged for debugging. From 0288f5a93e347792426a97853c74c150125ea9e8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:10:35 +0100 Subject: [PATCH 380/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20navigation=20an?= =?UTF-8?q?d=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index c9dc6b3..0433917 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,9 +7,9 @@ repo_url: https://github.com/shroominic/funcchain/ nav: - "Funcchain": "index.md" - "Getting Started": - - "Introduction": "getting-started/introduction.md" - "Installation": "getting-started/installation.md" - "Usage": "getting-started/usage.md" + - "Demos": "getting-started/demos.md" - "Configuration": "getting-started/config.md" - "Models": "getting-started/models.md" - "Concepts": @@ -91,8 +91,8 @@ markdown_extensions: - pymdownx.magiclink: normalize_issue_symbols: true repo_url_shorthand: true - user: jxnl - repo: instructor + user: shroominic + repo: funcchain - pymdownx.mark - pymdownx.smartsymbols - pymdownx.snippets: From 8dfa095ecd3ec4aa0834e45c07d4e011d93ab155 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:41:54 +0100 Subject: [PATCH 381/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20LangChain=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/langchain.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/concepts/langchain.md b/docs/concepts/langchain.md index 54328d7..de59747 100644 --- a/docs/concepts/langchain.md +++ b/docs/concepts/langchain.md @@ -2,10 +2,19 @@ ## What is LangChain? +[LangChain](https://python.langchain.com/docs/get_started/introduction) is the most advanced library for building applications using large language models. +Funcchain is built on top of `langchain_core` which inculdes [LangChain Expression Language (LCEL)](https://python.langchain.com/docs/expression_language/get_started) and alot more powerful core abstractions for building cognitive architectures. + ## Why building on top of it? +We have been looking into alot of different libraries and wanted to start a llm framework from scratch. +But langchain already provides alot of the core abstractions we need to use and it is the most advanced library we have found so far. + ## Compatibility -## Runnables +Funcchain is compatible with all langchain chat models, memory stores and runnables. +It's using langchain output parsers and the templating system. +On the other hand langchain is compatible with funcchain by using the `@runnable` decorator. +This will convert your function into a runnable that can be used in LCEL so you can build your own complex cognitive architectures. -## Extension/Compatibility Examples +## TODO: add runnable example From b116b8375ba404a11bf619dc252218b3aa6b0dda Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:41:54 +0100 Subject: [PATCH 382/451] =?UTF-8?q?=F0=9F=93=9A=20Add=20LlamaCPP=20and=20g?= =?UTF-8?q?rammars=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/local-models.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/concepts/local-models.md b/docs/concepts/local-models.md index 5dee660..ff95c36 100644 --- a/docs/concepts/local-models.md +++ b/docs/concepts/local-models.md @@ -1,5 +1,21 @@ # Local Models +Funcchain supports local models through the [llama.cpp](https://github.com/ggerganov/llama.cpp) project using the [llama_cpp_python](https://llama-cpp-python.readthedocs.io/en/latest/) bindings. + ## LlamaCPP +Written in highly optimized C++ code, LlamaCPP is a library for running large language models locally. +It uses GGUF files which are a binary format for storing quantized versions of large language models. +You can download alot of GGUF models from TheBloke on huggingface. + ## Grammars + +Context Free Grammars are a powerful abstraction for a deterministic shape of a string. +Funcchain utilizes this by forcing local models to respond in a structured way. + +For example you can create a grammar that forces the model to always respond with a json object. +This is useful if you want to use the output of the model in your code. + +Going one step further you can also create a grammar that forces the model to respond with a specific pydantic model. + +This is how funcchain is able to use local models in a structured way. From 8ecbe3dd114090f49e9f48298500287db357dbc2 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 30 Jan 2024 17:41:54 +0100 Subject: [PATCH 383/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20introd?= =?UTF-8?q?uction.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/introduction.md | 468 --------------------------- 1 file changed, 468 deletions(-) delete mode 100644 docs/getting-started/introduction.md diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md deleted file mode 100644 index 5e52abc..0000000 --- a/docs/getting-started/introduction.md +++ /dev/null @@ -1,468 +0,0 @@ - -# Introduction - -[![Version](https://badge.fury.io/py/funcchain.svg)](https://badge.fury.io/py/funcchain) -[![tests](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml/badge.svg)](https://github.com/shroominic/funcchain/actions/workflows/code-check.yml) -![PyVersion](https://img.shields.io/pypi/pyversions/funcchain) -![Downloads](https://img.shields.io/pypi/dm/funcchain) -[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://docs.pydantic.dev/latest/contributing/#badges) -[![Twitter Follow](https://img.shields.io/twitter/follow/shroominic?style=social)](https://x.com/shroominic) - -
- - ```bash - $ pip install funcchain - ---> 100% - ``` - -
- -`funcchain` is the _most pythonic_ way of writing cognitive systems. Leveraging pydantic models as output schemas combined with langchain in the backend allows for a seamless integration of llms into your apps. -It works perfect with OpenAI Functions and soon with other models using JSONFormer. - -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/ricklamers/funcchain-demo) - -## Simple Demo - -```python -from funcchain import chain -from pydantic import BaseModel - -# define your output shape -class Recipe(BaseModel): - ingredients: list[str] - instructions: list[str] - duration: int - -# write prompts utilising all native python features -def generate_recipe(topic: str) -> Recipe: - """ - Generate a recipe for a given topic. - """ - return chain() # <- this is doing all the magic - -# generate llm response -recipe = generate_recipe("christmas dinner") - -# recipe is automatically converted as pydantic model -print(recipe.ingredients) -``` - -!!! Step-by-Step - ```python - # define your output shape - class Recipe(BaseModel): - ingredients: list[str] - instructions: list[str] - duration: int - - ``` - - A Recipe class is defined, inheriting from BaseModel (pydantic library). This class - specifies the structure of the output data, which you can customize. - In the example it includes a list of ingredients, a list of instructions, and an integer - representing the duration - - ```python - # write prompts utilising all native python features - def generate_recipe(topic: str) -> Recipe: - """ - Generate a recipe for a given topic. - """ - return chain() # <- this is doing all the magic - ``` - In this example the `generate_recipe` function takes a topic string and returns a `Recipe` instance for that topic. - # Understanding chain() Functionality - Chain() is the backend magic of funcchain. Behind the szenes it creates the prompt executable from the function signature. - Meaning it will turn your function into usable LLM input. - - The `chain()` function does the interaction with the language model to generate a recipe. It accepts several parameters: `system` to specify the model, `instruction` for model directives, `context` to provide relevant background information, `memory` to maintain conversational state, `settings_override` for custom settings, and `**input_kwargs` for additional inputs. Within `generate_recipe`, `chain()` is called with arguments derived from the function's parameters, the function's docstring, or the library's default settings. It compiles these into a Runnable, which then prompts the language model to produce the output. This output is automatically structured into a `Recipe` instance, conforming to the Pydantic model's schema. - - # Get your response - ```python - # generate llm response - recipe = generate_recipe("christmas dinner") - - # recipe is automatically converted as pydantic model - print(recipe.ingredients) - ``` - -## Demo - -
- ``` - $ print(generate_recipe("christmas dinner").ingredients - - ['turkey', 'potatoes', 'carrots', 'brussels sprouts', 'cranberry sauce', 'gravy', - 'butter', 'salt', 'pepper', 'rosemary'] - - ``` -
- -## Complex Structured Output - -```python -from pydantic import BaseModel, Field -from funcchain import chain - -# define nested models -class Item(BaseModel): - name: str = Field(description="Name of the item") - description: str = Field(description="Description of the item") - keywords: list[str] = Field(description="Keywords for the item") - -class ShoppingList(BaseModel): - items: list[Item] - store: str = Field(description="The store to buy the items from") - -class TodoList(BaseModel): - todos: list[Item] - urgency: int = Field(description="The urgency of all tasks (1-10)") - -# support for union types -def extract_list(user_input: str) -> TodoList | ShoppingList: - """ - The user input is either a shopping List or a todo list. - """ - return chain() - -# the model will choose the output type automatically -lst = extract_list( - input("Enter your list: ") -) - -# custom handler based on type -match lst: - case ShoppingList(items=items, store=store): - print("Here is your Shopping List: ") - for item in items: - print(f"{item.name}: {item.description}") - print(f"You need to go to: {store}") - - case TodoList(todos=todos, urgency=urgency): - print("Here is your Todo List: ") - for item in todos: - print(f"{item.name}: {item.description}") - print(f"Urgency: {urgency}") -``` - -!!! Step-by-Step - **Nececary Imports** - - ```python - from pydantic import BaseModel, Field - from funcchain import chain - ``` - - **Data Structures and Model Definitions** - ```python - # define nested models - class Item(BaseModel): - name: str = Field(description="Name of the item") - description: str = Field(description="Description of the item") - keywords: list[str] = Field(description="Keywords for the item") - - class ShoppingList(BaseModel): - items: list[Item] - store: str = Field(description="The store to buy the items from") - - class TodoList(BaseModel): - todos: list[Item] - urgency: int = Field(description="The urgency of all tasks (1-10)") - - ``` - - In this example, Funcchain utilizes Pydantic models to create structured data schemas that facilitate the processing of programmatic inputs. - - You can define new Pydantic models or extend existing ones by adding additional fields or methods. The general approach is to identify the data attributes relevant to your application and create corresponding model classes with these attributes. - - - **Union types** - ```python - # support for union types - def extract_list(user_input: str) -> TodoList | ShoppingList: - """ - The user input is either a shopping List or a todo list. - """ - return chain() - ``` - The extract_list function uses the chain function to analyze user input and return a structured list: - In the example: - - Union Types: It can return either a TodoList or a ShoppingList, depending on the input. - - Usage of chain: chain simplifies the process, deciding the type of list to return. - - For your application this is going to serve as a router to route between your previously defined models. - - **Get a list from the user** (here as "lst") - ```python - # the model will choose the output type automatically - lst = extract_list( - input("Enter your list: ") - ) - - ``` - - **Define your custom handlers** - - And now its time to define what happens with the result. - You can then use the lst variable to match. - - ```python - # custom handler based on type - match lst: - case ShoppingList(items=items, store=store): - print("Here is your Shopping List: ") - for item in items: - print(f"{item.name}: {item.description}") - print(f"You need to go to: {store}") - - case TodoList(todos=todos, urgency=urgency): - print("Here is your Todo List: ") - for item in todos: - print(f"{item.name}: {item.description}") - print(f"Urgency: {urgency}") - - ``` - -
- ``` - lst = extract_list( - input("Enter your list: ") - ) - - User: - $ Complete project report, Prepare for meeting, Respond to emails; - $ if I don't respond I will be fired - - Output: - $ ............... - Here is your Todo List: - Complete your buisness tasks: project report, Prepare for meeting, Respond to emails - Urgency: 10 - //add real output - ``` - -
- -## Vision Models - -```python -from PIL import Image -from pydantic import BaseModel, Field -from funcchain import chain, settings - -# set global llm using model identifiers (see MODELS.md) -settings.llm = "openai/gpt-4-vision-preview" - -# everything defined is part of the prompt -class AnalysisResult(BaseModel): - """The result of an image analysis.""" - - theme: str = Field(description="The theme of the image") - description: str = Field(description="A description of the image") - objects: list[str] = Field(description="A list of objects found in the image") - -# easy use of images as input with structured output -def analyse_image(image: Image.Image) -> AnalysisResult: - """ - Analyse the image and extract its - theme, description and objects. - """ - return chain() - -result = analyse_image(Image.open("examples/assets/old_chinese_temple.jpg")) - -print("Theme:", result.theme) -print("Description:", result.description) -for obj in result.objects: - print("Found this object:", obj) -``` - -!!! Step-by-Step - **Nececary Imports** - - ```python - from PIL import Image - from pydantic import BaseModel, Field - from funcchain import chain, settings - ``` - - **Define Model** - set global llm using model identifiers see [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) - ```python - settings.llm = "openai/gpt-4-vision-preview" - ``` - Funcchains modularity allows for all kinds of models including local models - - - **Analize Image** - Get structured output from an image in our example `theme`, `description` and `objects` - ```python - # everything defined is part of the prompt - class AnalysisResult(BaseModel): - """The result of an image analysis.""" - - theme: str = Field(description="The theme of the image") - description: str = Field(description="A description of the image") - objects: list[str] = Field(description="A list of objects found in the image") - ``` - Adjsut the fields as needed. Play around with the example, feel free to experiment. - You can customize the analysis by modifying the fields of the `AnalysisResult` model. - - **Function to start the analysis** - - ```python - # easy use of images as input with structured output - def analyse_image(image: Image.Image) -> AnalysisResult: - """ - Analyse the image and extract its - theme, description and objects. - """ - return chain() - ``` - Chain() will handle the image input. - We here define again the fields from before `theme`, `description` and `objects` - - give an image as input `image: Image.Image` - - Its important that the fields defined earlier are mentioned here with the prompt - `Analyse the image and extract its`... - -
- ``` - print(analyse_image(image: Image.Image)) - - $ .................. - - Theme: Nature - Description: A beautiful landscape with a mountain range in the background, a clear blue sky, and a calm lake in the foreground surrounded by greenery. - Found this object: mountains - Found this object: sky - Found this object: lake - Found this object: trees - Found this object: grass - - ``` - -
- -## Seamless local model support - -Yes you can use funcchain without internet connection. -Start heating up your device. - -```python -from pydantic import BaseModel, Field -from funcchain import chain, settings - -# auto-download the model from huggingface -settings.llm = "ollama/openchat" - -class SentimentAnalysis(BaseModel): - analysis: str - sentiment: bool = Field(description="True for Happy, False for Sad") - -def analyze(text: str) -> SentimentAnalysis: - """ - Determines the sentiment of the text. - """ - return chain() - -# generates using the local model -poem = analyze("I really like when my dog does a trick!") - -# promised structured output (for local models!) -print(poem.analysis) -``` - -!!! Step-by-Step - **Nececary Imports** - - ```python - from pydantic import BaseModel, Field - from funcchain import chain, settings - ``` - - **Choose and enjoy** - ```python - # auto-download the model from huggingface - settings.llm = "ollama/openchat" - ``` - - **Structured output definition** - With an input `str` a description can be added to return a boolean `true` or `false` - ```python - class SentimentAnalysis(BaseModel): - analysis: str - sentiment: bool = Field(description="True for Happy, False for Sad") - ``` - Experiment yourself by adding different descriptions for the true and false case. - - **Use `chain()` to analize** - Defines with natural language the analysis - ```python - def analyze(text: str) -> SentimentAnalysis: - """ - Determines the sentiment of the text. - """ - return chain() - ``` - For your own usage adjust the str. Be precise and reference your classes again. - - **Generate and print the output** - ```python - **Use the analyze function and print output** - - # generates using the local model - poem = analyze("I really like when my dog does a trick!") - - # promised structured output (for local models!) - print(poem.analysis) - ``` - -
- -!!! Useful: Langsmith integration - - Add those lines to .env and funcchain will use langsmith tracing. - - ```bash - LANGCHAIN_TRACING_V2=true - LANGCHAIN_API_KEY="ls__api_key" - LANGCHAIN_PROJECT="PROJECT_NAME" - ``` - - Langsmith is used to understand what happens under the hood of your LLM generations. - When multiple LLM calls are used for an output they can be logged for debugging. - -## Features - -- **🎨 Minimalistic and Easy to Use**: Designed with simplicity in mind for straightforward usage. -- **🔄 Model Flexibility**: Effortlessly switch between OpenAI and local models. -- **📝 Pythonic Prompts**: Craft natural language prompts as intuitive Python functions. -- **🔧 Structured Output**: Define output schemas with Pydantic models. -- **🚀 Powered by LangChain**: Utilize the robust LangChain core for backend operations. -- **🧩 Template Support**: Employ f-strings or Jinja templates for dynamic prompt creation. -- **🔗 Integration with AI Services**: Take full advantage of OpenAI Functions or LlamaCpp Grammars. -- **🛠️ Langsmith Support**: Ensure compatibility with Langsmith for superior language model interactions. -- **⚡ Asynchronous and Pythonic**: Embrace modern Python async features. -- **🤗 Huggingface Integration**: Automatically download models from Huggingface. -- **🌊 Streaming Support**: Enable real-time streaming for interactive applications. - -## Documentation - -Highly recommend to try out the examples in the `./examples` folder. - -Coming soon... feel free to add helpful .md files :) - -## Contribution - -You want to contribute? That's great! Please run the dev setup to get started: - -```bash -> git clone https://github.com/shroominic/funcchain.git && cd funcchain - -> ./dev_setup.sh -``` - -Thanks! From b295f8a18055f1848488098b232c02edf6d26317 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Wed, 31 Jan 2024 14:21:01 +0400 Subject: [PATCH 384/451] =?UTF-8?q?=F0=9F=93=9D=20update=20todos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roadmap.todo | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/roadmap.todo b/roadmap.todo index bc4b009..c9197c7 100644 --- a/roadmap.todo +++ b/roadmap.todo @@ -1,12 +1,26 @@ -IMPORTANT NOW: -[ ] - improve docs (8h) +V0.2: +[ ] - write docs (8h) -TODO: +V0.2.1 +[ ] - pygmalion grammar support -[ ] - enable union type without function calling (6h) +V0.3: + +[ ] - outlines as (optional) backend for guided generation (mostly json-schemas) + +[ ] - dspy integration for autotuning prompts [ ] - pydantic model streaming (6h) +[ ] - enable union type without function calling (6h) + +[ ] - enable Error type for non union calls (4h) + +[ ] - convert langchain tools to funcchain agent/router (8h) + + +V0.4+: + [ ] - cookbooks folder with jupyter notebook tutorials (8h) [ ] - depends functionality to create nested chains and compile into runnables (10h) @@ -22,12 +36,8 @@ TODO: # So anything that is additional can be compressed to fit in the context but when other things that are important are not compressed. # Optionally you can define how to compress and where to leave the gaps (default in the middle with [...]) -[ ] - enable Error type for non union calls (4h) - [ ] - LLMCompiler written in funcchain example (30h) -[ ] - convert langchain tools to funcchain agent/router (8h) - [ ] - vscode extension for custom syntax highlighting (30h) [ ] - parallel function calling (8h) From b1ed7f9084cd8541169d7f3e5130a39fb912a593 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 385/451] =?UTF-8?q?=E2=9C=A8=20Add=20async=20documentation?= =?UTF-8?q?=20TODOs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/async.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/advanced/async.md b/docs/advanced/async.md index f82740c..a26eee2 100644 --- a/docs/advanced/async.md +++ b/docs/advanced/async.md @@ -2,12 +2,24 @@ ## Why and how to use using async? +### TODO + ## How to use async? +### TODO + ## Async Examples +### TODO + ## Async in FuncChain +### TODO + ## Async in LangChain +### TODO + ## Async Streaming + +### TODO From a0b7ac1c813619773f3c0b8edd6de7975279170f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 386/451] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20Add=20TODOs=20for?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/codebase-scaling.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced/codebase-scaling.md b/docs/advanced/codebase-scaling.md index 6c52d8d..ce79a89 100644 --- a/docs/advanced/codebase-scaling.md +++ b/docs/advanced/codebase-scaling.md @@ -2,4 +2,8 @@ ## Multi file projects +### TODO + ## Structure + +### TODO From 02524edda2456a89115137cdf5775f556a363e4a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 387/451] =?UTF-8?q?=E2=9C=A8=20Add=20TODO=20placeholders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/custom-parser-types.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/advanced/custom-parser-types.md b/docs/advanced/custom-parser-types.md index df83448..a0168b5 100644 --- a/docs/advanced/custom-parser-types.md +++ b/docs/advanced/custom-parser-types.md @@ -2,10 +2,20 @@ ## Example +### TODO + ## Grammars +### TODO + ## Format Instructions +### TODO + ## parse() Function +### TODO + ## Write your own Parser + +### TODO From 68d9da5faaf582e514dc080c981faf76576a34f3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 388/451] =?UTF-8?q?=E2=9C=A8=20Add=20TODO=20placeholders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/customization.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/advanced/customization.md b/docs/advanced/customization.md index ad52637..790fb18 100644 --- a/docs/advanced/customization.md +++ b/docs/advanced/customization.md @@ -2,8 +2,16 @@ ## extra args inside chain +### TODO + ## low level langchain +### TODO + ## extra args inside @runnable +### TODO + ## custom ll models + +### TODO From e28a3a19528b844689e9efda7d15811fdac3dac1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 389/451] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20Add=20TODO=20place?= =?UTF-8?q?holders=20in=20runnables.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/runnables.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced/runnables.md b/docs/advanced/runnables.md index 026fb01..9e6176c 100644 --- a/docs/advanced/runnables.md +++ b/docs/advanced/runnables.md @@ -2,4 +2,8 @@ ## LangChain Expression Language (LCEL) +### TODO + ## Streaming, Parallel, Async and + +### TODO From 555bc36412f4a639544dc4d413fb94d40e05643a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 390/451] =?UTF-8?q?=E2=9C=A8=20Add=20TODO=20placeholders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/signature.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced/signature.md b/docs/advanced/signature.md index 8b806c5..97a30cd 100644 --- a/docs/advanced/signature.md +++ b/docs/advanced/signature.md @@ -2,4 +2,8 @@ ## Compilation +### TODO + ## Schema + +### TODO From 6f12e213f22fab439d38e6617798ccf31902cbbb Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 391/451] =?UTF-8?q?=F0=9F=93=98=20Add=20stream=20parsing?= =?UTF-8?q?=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/stream-parsing.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/advanced/stream-parsing.md b/docs/advanced/stream-parsing.md index e69de29..6f06e58 100644 --- a/docs/advanced/stream-parsing.md +++ b/docs/advanced/stream-parsing.md @@ -0,0 +1,9 @@ +# Stream Parsing + +## Transform Output Parsing + +### TODO + +## Composing Runnables + +### TODO From ac21ea7ac31c95b4114ab851ede321bc83d0e57c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 392/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20chain=20documen?= =?UTF-8?q?tation=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/chain.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/concepts/chain.md b/docs/concepts/chain.md index 9c0b87c..bf7f5c4 100644 --- a/docs/concepts/chain.md +++ b/docs/concepts/chain.md @@ -1,9 +1,8 @@ # Chain -## chain( ) method +## `chain()` -explain about chain like in usage.md and show achain and @runnable -exalain a bit how chain works under the hood +The chain function abstracts away all the magic happening in the funcchain backend. It extracts the docstring, input arguments and return type of the function and compiles everything into a langchain prompt. ```python from funcchain import chain @@ -18,9 +17,9 @@ ask("What is the capital of Germany?") # => "The capital of Germany is Berlin." ``` -## achain( ) method +## `achain()` -Async version of the chain() method. +Async version of the `chain()` function. ```python import asyncio @@ -36,10 +35,10 @@ asyncio.run(ask("What is the capital of Germany?")) # => "The capital of Germany is Berlin." ``` -## runnable decorator +## `@runnable` The `@runnable` decorator is used to compile a chain function into a langchain runnable object. -You just write a normal funcchain function using chain() and then decorate it with @runnable. +You just write a normal funcchain function using chain() and then decorate it with `@runnable`. ```python from funcchain import chain, runnable From b7c1cb479fc904f58d8de58961895d2893e8ce3d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 393/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20extra=20whitesp?= =?UTF-8?q?ace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/errors.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/concepts/errors.md b/docs/concepts/errors.md index d6ac68a..a9d6661 100644 --- a/docs/concepts/errors.md +++ b/docs/concepts/errors.md @@ -6,12 +6,10 @@ from funcchain import BaseModel, Error, chain from rich import print - class User(BaseModel): name: str email: str | None - def extract_user_info(text: str) -> User | Error: """ Extract the user information from the given text. From 057259e4e585c6e099bc296e247f1b476a012ef6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 394/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20images=20section?= =?UTF-8?q?=20placeholder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/input.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/concepts/input.md b/docs/concepts/input.md index 39175b4..ad2a075 100644 --- a/docs/concepts/input.md +++ b/docs/concepts/input.md @@ -97,6 +97,10 @@ def create_username(user: User) -> str: return chain() ``` +## Images + +todo: write + ## Other Types More special types are coming soon. From 79b8237d4e8020539183e239a7a7050431c7b4d5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 395/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20langchain=20doc?= =?UTF-8?q?umentation=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/langchain.md | 44 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/concepts/langchain.md b/docs/concepts/langchain.md index de59747..26b5e18 100644 --- a/docs/concepts/langchain.md +++ b/docs/concepts/langchain.md @@ -8,7 +8,7 @@ Funcchain is built on top of `langchain_core` which inculdes [LangChain Expressi ## Why building on top of it? We have been looking into alot of different libraries and wanted to start a llm framework from scratch. -But langchain already provides alot of the core abstractions we need to use and it is the most advanced library we have found so far. +But langchain already provides all of the fundamental abstractions we need to use and it is the most advanced library we have found so far. ## Compatibility @@ -17,4 +17,44 @@ It's using langchain output parsers and the templating system. On the other hand langchain is compatible with funcchain by using the `@runnable` decorator. This will convert your function into a runnable that can be used in LCEL so you can build your own complex cognitive architectures. -## TODO: add runnable example +## LCEL Example (RAG) + +```python +from funcchain import chain, runnable +from langchain_community.vectorstores.faiss import FAISS +from langchain_core.runnables import Runnable, RunnablePassthrough +from langchain_openai.embeddings import OpenAIEmbeddings + +@runnable +def generate_poem(topic: str, context: str) -> str: + """ + Generate a poem about the topic with the given context. + """ + return chain() + +vectorstore = FAISS.from_texts( + [ + "cold showers are good for your immune system", + "i dont like when people are mean to me", + "japanese tea is full of heart warming flavors", + ], + embedding=OpenAIEmbeddings(), +) +retriever = vectorstore.as_retriever(search_kwargs={"k": 1}) + +retrieval_chain: Runnable = { + "context": retriever, + "topic": RunnablePassthrough(), +} | generate_poem + +print(retrieval_chain.invoke("love")) +``` + +The chain will then retrieve ´japanese tea is full of heart warming flavors` as context since it's the most similar to the topic "love". + +```bash +# => In a cup of tea, love's warmth unfurls +# Japanese flavors, heartwarming pearls +# A sip of love, in every swirl +# In Japanese tea, love's essence twirls +``` From 41b38c667be86e42e10903bfd7bc3f3068e2924f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 396/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20parser=20docume?= =?UTF-8?q?ntation=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/parser.md | 100 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/concepts/parser.md b/docs/concepts/parser.md index f40e077..b90caa7 100644 --- a/docs/concepts/parser.md +++ b/docs/concepts/parser.md @@ -2,12 +2,110 @@ ## Output Type Hints +Funcchain recognises the output type hint you put on your function to automatically attach +a fitting output parser to the end of your chain. This makes it really to code because you just use normal python typing syntax and funcchain handles everything for your. + ## Strings -The simplest output type is a string. The output parser will return the content of the AI response just as it is. +The simplest output type is a string. +The output parser will return the content of the AI response just as it is. ## Pydantic Models +To force the model to respond in a certain way you can use pydantic models. +This gives your alot of flexibility and control over the output because you can define the exact types of your fields and even add custom validation logic. Everything of your defined model will be part of the prompt including model_name, class_docstring, field_names, field_types and field_descriptions. +This gives you alot of room for prompt engineering and optimisation. + +```python +from funcchain import chain +from pydantic import BaseModel, Field + +class GroceryList(BaseModel): + recipie: str = Field(description="Goal of what to cook with all items.") + items: list[str] = Field(description="Items to buy") + +def create_grocerylist(customer_request: str) -> GroceryList + """ + Come up with a grocery list based on what the customer wants. + """ + return chain() +``` + +When calling this function with +e.g. `create_grocerylist("I want a cheap, protein rich and vegan meal.")` +the model is then forced to respond using the model as a json_schema +and the unterlying conversation would look like the following: + +```html + + CUSTOMER_REQUEST: + I want a cheap, protein rich and vegan meal. + + Come up with a grocery list based on what the customer wants. + + + + { + "recipie": "lentil soup" + "items" [ + "todo", + "insert", + "ingredients" + ] + } + +``` + +This json is then automatically validated and parsed into the pydantic model. +When a validation fails the model automatically recieves the error as followup message and tries again. + ## Primitive Types +You can also use other primitive types like int, float, bool, list, Literals, Enums, etc.
+Funcchain will then create a temporary pydantic model with the type as a single field and use that as the output parser. + +```python +def create_grocerylist(customer_request: str) -> list[str] + """ + Come up with a grocery list based on what the customer wants. + """ + return chain() +``` + +This time when calling this function with +e.g. `create_grocerylist("I want a cheap, protein rich and vegan meal.")` +funcchain automatically creates a temporary pydantic model in the background like this: + +```python +class Extract(BaseModel): + value: list[str] +``` + +The model then understands the desired shape and will output like here: + +```html + + { + "value": [ + "todo", + "insert", + "ingredients" + ] + } + +``` + +## Union Types + +You can also use mupliple PydanticModels at once as the output type using Unions. +The LLM will then select one of the models that fits best your inputs. +Checkout the seperate page for [UnionTypes](unions.md) for more info. + ## Streaming + +You can stream everything with a `str` output type. + +Since pydantic models need to be fully constructed before they can be returned, you can't use them for streaming. +There is one approach to stream pydantic models but it works only if all fields are Optional, which is not the case for most models and they still come field by field. + +This is not implemented yet but will be added in the future. From 21500ddbe346a39730e6d267168ee700575d7c73 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 397/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20prompting=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/prompting.md | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/concepts/prompting.md b/docs/concepts/prompting.md index 8cc0be7..84c87f3 100644 --- a/docs/concepts/prompting.md +++ b/docs/concepts/prompting.md @@ -1,9 +1,52 @@ # Prompting +Prompting involes every text you write that the LLM recieves and interprets. It often involves `prompt engineering` which is optimizing and finetuning your wordings so the LLM understands what you wants and responds correctly. +Everything from the input argument names, output type and docstring are part of the prompt and are visible to the model to evaluate. Make sure your choose your terms well and try different wordings if you encounter problems. + ## Jinja2 Templating +Often you can write your funcchains without templating but for special cases it is useful to do custom things. +Funcchain allows jinja2 as templating syntax for writing more complex prompts. +All function input areguments that are either `str` or a subclass of a `Pydantic Model` are awailable in the jinja environment and can be used. + +```python +class GroceryList(BaseModel): + recipie: str + items: list[str] + +def create_recipie(glist: GroceryList) -> str: + """ + I want to cook {{ glist.recipie }}. + Create a step by step recipie based on these ingridients I just bought: + {% for i in glist.items %} + - {{ i }} + {% endfor %} + """ + return chain() +``` + +The LLM will then recieve a formatted prompt based on what you input is. + ## Input Argument Placement +If you do not specify a place in your prompt for your input arguments using jinja, +all unused arguments (`str` and `PydanticModels`) will then get automatically appended +to the beginning of your instruction. + +E.g. if you just provide `Create a step by step recipie based on the grocery list.`, +the prompt template would look like this: + +```html + + GLIST: + {{ glist }} + + Create a step by step recipie based on the grocery list. + +``` + +When inserting the instance of `GroceryList` into the template, the `__str__` method is called for converting the model into text. Keep this in mind if you want to customise apperence to the LLM. + ## ChatModel Behavior -## Compilation Examples +Keep in mind that funcchain in always using instructional chat prompting, so instrution is made the perspective of a Human <-> AI conversation. If you process input from your users its good to talk of them as `customers` so the model understands the perspective. From 30d4800f218c6bb2c510e6485d5e17c157d9c337 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 398/451] =?UTF-8?q?=F0=9F=93=9A=20Add=20Pydantic=20documen?= =?UTF-8?q?tation=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/pydantic.md | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/concepts/pydantic.md b/docs/concepts/pydantic.md index e69de29..a205df4 100644 --- a/docs/concepts/pydantic.md +++ b/docs/concepts/pydantic.md @@ -0,0 +1,68 @@ +# Pydantic + +`pydantic` is a python library for creating data structures with strict typing and automatic type validation. +When dealing with LLMs this is very useful because it exposes a precise `json-schema` that can be used with grammars or function-calling to force the LLM to respond in the desired way. +Additionally validation especially using custom validators can be used to automatically retry if the output does not match your requirements. + +## BaseModel + +When `from pydantic import BaseModel` this is imports the core class of Pydantic which can be used to construct your data structures. + +```python +from pydantic import BaseModel + +class User(BaseModel): + id: int + name: str + email: str + items: list[str] +``` + +This model can then be initiated: + +```python +user = User( + id=1943, + name="Albert Hofmann", + email="hofmann.albert@sandoz.ch", + items=["lab coat", "safety glasses", "a bicycle"] +) +``` + +## Field Descriptions + +To give the LLM more context about what you mean with the stucture you can provide field descriptions: + +```python +from pydantic import Field + +class User(BaseModel): + id: int + name: str = Field(description="FullName of the user.") + email: str + items: list[str] = Field(description="Everyday items of the user.") +``` + +These descriptions are included in the json-schema and are passed as format instructions into the prompt from the output parser. + +## Custom Validators + +You can also write custom validators if you want to check for specific information beyond just the type. + +```python +from pydantic import field_validator + +class User(BaseModel): + id: int + name: str = Field(description="FullName of the user.") + email: str + items: list[str] = Field(description="Everyday items of the user.") + + @field_validator("email") + def keywords_must_be_unique(cls, v: str) -> str: + if not v.endswith("@sandoz.ch"): + raise ValueError("User has to work at Sandoz to register!") + return v +``` + +In this example the validator makes sure every user has an email ending with `@sandoz.ch`. From 5220cda5dec2b62989df3b915400726c3db20b9a Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 399/451] =?UTF-8?q?=F0=9F=93=9A=20Expand=20streaming=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/streaming.md | 45 ++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index 39c0b6c..feb4497 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -1,9 +1,46 @@ # Streaming -## How to use streaming? +Streaming is important if you want to do things with your LLM generation while the LLM is still generating. +This can enhance the user experience by already showing part of the response but you could also stop a generation early if it does not match certain requirements. -## runnable streaming +## Console Log Streaming -## Console Streaming +If you want to stream all the tokens generated quickly to your console output, +you can use the `settings.console_stream = True` setting. -## Streaming Examples +## `strem_to()` wrapper + +For streaming with non runnable funcchains you can wrap the LLM generation call into the `stream_to()` context manager. This would look like this: + +```python +def summarize(text: str) -> str: + """Summarize the text.""" + return chain() + +text = "... a large text" + +with stream_to(print): + summarize(text) +``` + +This will call token by token the print function so it will show up in your console. +But you can also insert any function that accepts a string to create your custom stream handlers. + +You can also use `async with stream_to(your_async_handler):` for async streaming. +Make sure summarize is then created using `await achain()`. + +## LangChain runnable streaming + +If you can compile every funcchain into a langchain runnable and then use the native langchain syntax for streaming: + +```python +@runnable +def summarize(text: str) -> str: + """Summarize the text.""" + return chain() + +text = "... a large text" + +for chunk in summarize.stream(input={"text": text}): + print(chunk, end="", flush=True) +``` From 4edf9d853b0bc304667865a91a4ba67964d832a1 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 400/451] =?UTF-8?q?=F0=9F=93=9A=20Update=20unions.md=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/unions.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/concepts/unions.md b/docs/concepts/unions.md index 26df726..4631039 100644 --- a/docs/concepts/unions.md +++ b/docs/concepts/unions.md @@ -1,7 +1,18 @@ # Union Types +You can use union types in funcchain to make the model select one of multiple PydanticModels for the response. +You may have seen this in the [Complex Example](../index.md#complex-example). + ## Errors +One good usecase for this is to always give the LLM the chance to raise an Error if the input is strange or not suited. You can check this in more detail [here](errors.md). + +## Agents + +Another usecase is to create an Agent like chain that selects one of multiple tools. +Every PydanticModel then represents the input schema of your function and you can even override the `__call__` method of your models to directly execute the tool if you need so. + ## Function Calling -## Model Selection +Under the hood the union type featur uses openai tool_calling, especially the functionallity to give the LLM multiple tools to choose from. +All pydantic models then get injected as available tools and the LLM is forced to call one of them. From d791f16fdb532f38b7816cf0d7f63161a8d213fa Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 401/451] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20Update=20vision?= =?UTF-8?q?=20models=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/vision.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/concepts/vision.md b/docs/concepts/vision.md index cd78885..7c2124d 100644 --- a/docs/concepts/vision.md +++ b/docs/concepts/vision.md @@ -1,9 +1,19 @@ # Vision Models -## GPT-4 Vision +Funcchain supports working with vision models so you can use images as input arguments of your prompts. +This only works if you also choose the correct model. +Currently known supported models: -## Local Vision Models +- `openai/gpt-4-vision-preview` +- `ollama/llava` or `ollama/bakllava` -## Examples +You need to set these using `settings.llm` (checkout the [Funcchain Settings](../getting-started/config.md)). ## Image Type + +`from funcchain import Image` + +Funcchain introuces a special type for Images to quickly recognise image arguments and format them correctly into the prompt. +This type also exposes a variaty of classmethods for creating but also methods for converting Image instances. + +Checkout the [Vision Example](../features/vision.md) for more details. From afd44fc2ff8e977c1def397401ff2af105690fce Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 402/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20code-o?= =?UTF-8?q?f-conduct.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/code-of-conduct.md | 43 ---------------------------- 1 file changed, 43 deletions(-) delete mode 100644 docs/contributing/code-of-conduct.md diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md deleted file mode 100644 index 148c2a8..0000000 --- a/docs/contributing/code-of-conduct.md +++ /dev/null @@ -1,43 +0,0 @@ -# Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org). From d3cbf173116939a12c99d269d093eff7c9aef3d9 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 403/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20codebase=20structu?= =?UTF-8?q?re=20placeholder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/codebase-structure.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/contributing/codebase-structure.md b/docs/contributing/codebase-structure.md index e69de29..2ca11eb 100644 --- a/docs/contributing/codebase-structure.md +++ b/docs/contributing/codebase-structure.md @@ -0,0 +1,3 @@ +# Codebase Structure + +## TODO: explain structure of codebase to easier contribute From b89fa5364d3a703af825f491c6befa307830efb7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 404/451] =?UTF-8?q?=F0=9F=93=9D=20Simplify=20contributors?= =?UTF-8?q?=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/contributors.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/contributing/contributors.md b/docs/contributing/contributors.md index 3fa741a..fbbe9e4 100644 --- a/docs/contributing/contributors.md +++ b/docs/contributing/contributors.md @@ -9,7 +9,3 @@ We would like to acknowledge the contributions of the following people: ## How to Contribute If you would like to contribute to this project, please follow the guidelines in our [Contributing Guide](dev-setup.md). - -## Code of Conduct - -Please note that this project is released with a [Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms. From 52d21c886b01995232c3ee28f35cd5452b206778 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 405/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20roadmap=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/roadmap.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/contributing/roadmap.md b/docs/contributing/roadmap.md index 8508f35..2b819eb 100644 --- a/docs/contributing/roadmap.md +++ b/docs/contributing/roadmap.md @@ -1,9 +1,9 @@ # TODOs for writing the documentation -- [ ] write out all scratched docs .md files +- [ ] write out all todos in advanced -- [ ] look into other repos for mkdocs inspiration +- [ ] look more into other repos for mkdocs tricks and inspiration -- [ ] make this file a todo list for contributors +- [ ] make this file a general todo list + roadmap for contributors -- [ ] add api reference? +- [ ] maybe rename features to examples From 8aa41668d1c6dc637c6aa3a2ec812757f9c38fea Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 406/451] =?UTF-8?q?=F0=9F=94=92=20Add=20security=20reporti?= =?UTF-8?q?ng=20instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/security.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/contributing/security.md b/docs/contributing/security.md index e69de29..c64f264 100644 --- a/docs/contributing/security.md +++ b/docs/contributing/security.md @@ -0,0 +1,3 @@ +# Security + +If you notice any security risks please immidiatly email `contact@shroominic.com` and for major risks you will recieve a bounty of 100$. From 2a28b06e46b011c514d8e77a784541c47e2a941b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 407/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20chat?= =?UTF-8?q?=20feature=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/chat.md | 126 ------------------------------------------ 1 file changed, 126 deletions(-) delete mode 100644 docs/features/chat.md diff --git a/docs/features/chat.md b/docs/features/chat.md deleted file mode 100644 index 8eb3ad6..0000000 --- a/docs/features/chat.md +++ /dev/null @@ -1,126 +0,0 @@ - -# ChatGPT rebuild with memory/history - -!!! Example - chatgpt.py [Example](https://github.com/shroominic/funcchain/blob/main/examples/chatgpt.py) - -!!! Important - Ensure you have set up your API key for the LLM of your choice, or Funcchain will look for a `.env` file. So in `.env` set up your key. - - ```bash - OPENAI_API_KEY="sk-rnUBxirFQ4bmz2Ae4qyaiLShdCJcsOsTg" - ``` - -## Code Example - -

-```python
-from funcchain import chain, settings
-from funcchain.utils.memory import ChatMessageHistory
-
-settings.llm = "openai/gpt-4"
-settings.console_stream = True
-
-history = ChatMessageHistory()
-
-def ask(question: str) -> str:
-    return chain(
-        system="You are an advanced AI Assistant.",
-        instruction=question,
-        memory=history,
-    )
-
-def chat_loop() -> None:
-    while True:
-        query = input("> ")
-
-        if query == "exit":
-            break
-
-        if query == "clear":
-            global history
-            history.clear()
-            print("\033c")
-            continue
-
-        ask(query)
-
-if __name__ == "__main__":
-    print("Hey! How can I help you?\n")
-    chat_loop()
-```
-
- -
- ```terminal - initial print function: - $ Hey! How can I help you? - $ > - - userprompt: - $ > Say that Funcchain is cool - - assistant terminal asnwer: - $ Funcchain is cool. - ``` - -
- -## Instructions - -!!! Step-by-Step - **Import nececary funcchain components** - - ```python - from funcchain import chain, settings - from funcchain.utils.memory import ChatMessageHistory - ``` - - **Settings** - - ```python - settings.llm = "openai/gpt-4" - settings.console_stream = True - ``` - - - Funcchain supports multiple LLMs and has the ability to stream received LLM text instead of waiting for the complete answer. For configuration options, see below: - - ```markdown - - `settings.llm`: Specify the language model to use. See MODELS.md for available options. - - Streaming: Set `settings.console_stream` to `True` to enable streaming, - or `False` to disable it. - ``` - - [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) - - - **Establish a chat history** - - ```python - history = ChatMessageHistory() - ``` - Stores messages in an in memory list. This will crate a thread of messages. - - See [memory.py] //Todo: Insert Link - - - **Define ask function** - See how funcchain uses `chain()` with an input `str` to return an output of type `str` - - ```python - def ask(question: str) -> str: - return chain( - system="You are an advanced AI Assistant.", - instruction=question, - memory=history, - ) - ``` - - This function sends a question to the Funcchain `chain()` function. - - It sets the system context as an advanced AI Assistant and passes the question as an instruction. - - The history object is used to maintain a thread of messages for context. - - The function returns the response from the chain function. From 653678602d335c048f716777403a6b3a4931fd43 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 408/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20dynamic=5Froute?= =?UTF-8?q?r.md=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/dynamic_router.md | 63 ++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md index bb798a5..7abf60c 100644 --- a/docs/features/dynamic_router.md +++ b/docs/features/dynamic_router.md @@ -98,12 +98,10 @@ router.invoke_route("Can you summarize this csv?") ## Demo
-```python -Input: -$ Can you summarize this csv? -$ ............... -Handling CSV requests with user query: Can you summarize this csv? -``` + ```python + $ router.invoke_route("Can you summarize this csv?") + Handling CSV requests with user query: Can you summarize this csv? + ```
## Instructions @@ -121,62 +119,69 @@ Handling CSV requests with user query: Can you summarize this csv? ``` **Define Route Type** + ```python class Route(TypedDict): - handler: Callable - description: str + handler: Callable + description: str ``` Create a `TypedDict` to define the structure of a route with a handler function and a description. Just leave this unchanged if not intentionally experimenting. **Implement Route Representation** + Establish a Router class + ```python class DynamicChatRouter(BaseModel): - routes: dict[str, Route] + routes: dict[str, Route] ``` **_routes_repr():** + Returns a string representation of all routes and their descriptions, used to help the language model understand the available routes. ```python def _routes_repr(self) -> str: - return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) + return "\n".join([f"{route_name}: {route['description']}" for route_name, route in self.routes.items()]) ``` **invoke_route(user_query: str, **kwargs: Any) -> Any: ** + This method takes a user query and additional keyword arguments. Inside invoke_route, an Enum named RouteChoices is dynamically created with keys corresponding to the route names. This Enum is used to validate the selected route. + ```python def invoke_route(self, user_query: str, /, **kwargs: Any) -> Any: - RouteChoices = Enum( # type: ignore - "RouteChoices", - {r: r for r in self.routes.keys()}, - type=str, - ) + RouteChoices = Enum( # type: ignore + "RouteChoices", + {r: r for r in self.routes.keys()}, + type=str, + ) ``` **Compile the Route Selection Logic** + The `RouterModel` class in this example is used for defining the expected output structure that the `compile_runnable` function will use to determine the best route for a given user query. ```python class RouterModel(BaseModel): - selector: RouteChoices = Field( - default="default", - description="Enum of the available routes.", - ) + selector: RouteChoices = Field( + default="default", + description="Enum of the available routes.", + ) route_query = compile_runnable( - instruction="Given the user query select the best query handler for it.", - input_args=["user_query", "query_handlers"], - output_type=RouterModel, + instruction="Given the user query select the best query handler for it.", + input_args=["user_query", "query_handlers"], + output_type=RouterModel, ) selected_route = route_query.invoke( - input={ - "user_query": user_query, - "query_handlers": self._routes_repr(), - } + input={ + "user_query": user_query, + "query_handlers": self._routes_repr(), + } ).selector assert isinstance(selected_route, str) @@ -195,19 +200,20 @@ Handling CSV requests with user query: Can you summarize this csv? Now you can use the structured output to execute programatically based on a natural language input. Establish functions tailored to your needs. + ```python def handle_pdf_requests(user_query: str) -> str: return "Handling PDF requests with user query: " + user_query - def handle_csv_requests(user_query: str) -> str: return "Handling CSV requests with user query: " + user_query - def handle_default_requests(user_query: str) -> str: return "Handling DEFAULT requests with user query: " + user_query ``` + **Define the routes** + And bind the previous established functions. ```python @@ -230,6 +236,7 @@ Handling CSV requests with user query: Can you summarize this csv? ``` **Get output** + Use the router.invoke_route method to process the user query and obtain the appropriate response. ```python From 828ae8bf9c4ae14b29abe5ebbc4de7a00f3c4b6e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 409/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20enums.md=20exam?= =?UTF-8?q?ple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/enums.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/features/enums.md b/docs/features/enums.md index 8086942..d1817ad 100644 --- a/docs/features/enums.md +++ b/docs/features/enums.md @@ -32,8 +32,7 @@ def make_decision(question: str) -> Decision: """ return chain() -if __name__ == "__main__": - print(make_decision("Do you like apples?")) +print(make_decision("Do you like apples?")) ```
@@ -41,10 +40,9 @@ if __name__ == "__main__":
```terminal - User: - $ Are apples red? - $ ............... - Decision(answer=) + $ make_decision("Do you like apples?") + + answer= ```
@@ -60,6 +58,7 @@ if __name__ == "__main__": ``` **Define the Answer Enum** + The Answer enum defines possible answers as 'yes' and 'no', which are the only valid responses for the decision-making system. Experiment by using and describing other enums. ```python @@ -67,7 +66,9 @@ if __name__ == "__main__": yes = "yes" no = "no" ``` + **Create the Decision Model** + The Decision class uses Pydantic to model a decision, ensuring that the answer is always an instance of the Answer enum. ```python @@ -76,6 +77,7 @@ if __name__ == "__main__": ``` **Implement the Decision Function** + The make_decision function is where the decision logic will be implemented, using `chain()` to process the question and return a decision. When using your own enums you want to edit this accordingly. @@ -88,11 +90,10 @@ if __name__ == "__main__": ``` **Run the Decision System** + This block runs the decision-making system, printing out the decision for a given question when the script is executed directly. ```python - if __name__ == "__main__": - print(make_decision("Do you like apples?")) - + print(make_decision("Do you like apples?")) ``` From 03d9d8552cc1e1c186a2f351f00f4ada4a775b50 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 410/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20error=5Foutput.?= =?UTF-8?q?md=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/error_output.md | 45 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/features/error_output.md b/docs/features/error_output.md index 0dccf55..bd12030 100644 --- a/docs/features/error_output.md +++ b/docs/features/error_output.md @@ -8,7 +8,6 @@ Most importantly we will be able to raise an error thats programmatically usable. You can adapt this for your own usage. - The main functionality is to take a string of text and attempt to extract user information, such as name and email, and return a User object. If the information is insufficient, an Error is returned instead. ## Full Code Example @@ -29,9 +28,11 @@ def extract_user_info(text: str) -> User | Error: """ return chain() -if __name__ == "__main__": - print(extract_user_info("hey")) # returns Error - print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object +print(extract_user_info("hey")) +# => returns Error + +print(extract_user_info("I'm John and my mail is john@gmail.com")) +# => returns a User object ```
@@ -40,17 +41,19 @@ Demo
```python - $ print(extract_user_info("hey")) - - Error: Insufficient information to extract user details. + $ extract_user_info("hey") - User: - $ print(extract_user_info("I'm John and my mail is john@gmail.com")) + Error( + title='Invalid Input', + description='The input text does not contain user information.' + ) - I'm John and my mail is john@gmail.com - User(name='John', email='john@gmail.com') + $ extract_user_info("I'm John and my mail is john@gmail.com") - //update example + User( + name='John', + email='john@gmail.com' + ) ```
@@ -60,22 +63,26 @@ Demo !!! Step-by-Step **Necessary Imports** + ```python from funcchain import BaseModel, Error, chain from rich import print ``` **Define the User Model** + ```python class User(BaseModel): - name: str - email: str | None + name: str + email: str | None ``` The User class is a Pydantic model that defines the structure of the user information to be extracted, with fields for `name` and an email. Change the fields to experiment and alignment with your project. **Implement the Extraction Function** + The `extract_user_info` function is intended to process the input text and return either a User object with extracted information or an Error if the information is not sufficient. + ```python def extract_user_info(text: str) -> User | Error: """ @@ -88,9 +95,13 @@ Demo **Run the Extraction System** + This conditional block is used to execute the extraction function and print the results when the script is run directly. + ```python - if __name__ == "__main__": - print(extract_user_info("hey")) # returns Error - print(extract_user_info("I'm John and my mail is john@gmail.com")) # returns a User object + print(extract_user_info("hey")) + # => returns Error + + print(extract_user_info("I'm John and my mail is john@gmail.com")) + # => returns a User object ``` From eb0b4de58d67b87f64cf0224e339b44c31b3833f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 411/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20literals=20docu?= =?UTF-8?q?mentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/literals.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/features/literals.md b/docs/features/literals.md index ee9b26d..b31caac 100644 --- a/docs/features/literals.md +++ b/docs/features/literals.md @@ -18,7 +18,7 @@ from funcchain import chain from pydantic import BaseModel class Ranking(BaseModel): - chain_of_thought: str + analysis: str score: Literal[11, 22, 33, 44, 55] error: Literal["no_input", "all_good", "invalid"] @@ -28,20 +28,20 @@ def rank_output(output: str) -> Ranking: """ return chain() -if __name__ == "__main__": - rank = rank_output("The quick brown fox jumps over the lazy dog.") - print(rank) +rank = rank_output("The quick brown fox jumps over the lazy dog.") +print(rank) ```
Demo
-```python -$ rank = rank_output("The quick brown fox jumps over the lazy dog.") -$ ........ -Ranking(chain_of_thought='...', score=33, error='all_good') -``` + ```python + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) + $ ........ + Ranking(analysis='...', score=33, error='all_good') + ```
## Instructions @@ -49,27 +49,28 @@ Ranking(chain_of_thought='...', score=33, error='all_good') !!! Step-by-Step **Necessary Imports** + ```python - from typing import Literal from funcchain import chain from pydantic import BaseModel - ``` **Define the Ranking Model** + The Ranking class is a Pydantic model that uses the Literal type to ensure that the score and error fields can only contain certain predefined values. So experiment with changing those but keeping this structure of the class. The LLM will be forced to deliver one of the defined output. ```python class Ranking(BaseModel): - chain_of_thought: str + analysis: str score: Literal[11, 22, 33, 44, 55] error: Literal["no_input", "all_good", "invalid"] ``` **Implement the Ranking Function** + Use `chain()` to process a user input, which must be a string. Adjust the content based on your above defined class. @@ -82,10 +83,10 @@ Ranking(chain_of_thought='...', score=33, error='all_good') ``` **Execute the Ranking System** + This block is used to execute the ranking function and print the results when the script is run directly. + ```python - - if __name__ == "__main__": - rank = rank_output("The quick brown fox jumps over the lazy dog.") - print(rank) + rank = rank_output("The quick brown fox jumps over the lazy dog.") + print(rank) ``` From 868efc1a67c5d3da80fd377b050c0118f1c9064d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 412/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20ollama=20exampl?= =?UTF-8?q?e=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/ollama.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/features/ollama.md b/docs/features/ollama.md index b433c76..c1ba902 100644 --- a/docs/features/ollama.md +++ b/docs/features/ollama.md @@ -5,7 +5,7 @@ See [ollama.py](https://github.com/shroominic/funcchain/blob/main/examples/ollama.py) Also see supported [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) - In this example, we will use the funcchain library to perform sentiment analysis on a piece of text. This showcases how funcchain can seamlessly utilize different Language Models (LLMs), such as ollama, without many unnececary code changes.. + In this example, we will use the funcchain library to perform sentiment analysis on a piece of text. This showcases how funcchain can seamlessly utilize different Language Models (LLMs) from ollama, without many unnececary code changes.. This is particularly useful for developers looking to integrate different models in a single application or just experimenting with different models. @@ -15,7 +15,6 @@ ```python from funcchain import chain, settings from pydantic import BaseModel, Field -from rich import print # define your model class SentimentAnalysis(BaseModel): @@ -31,7 +30,8 @@ def analyze(text: str) -> SentimentAnalysis: if __name__ == "__main__": # set global llm - settings.llm = "ollama/wizardcoder:34b-python-q3_K_M" + settings.llm = "ollama/openchat" + # log tokens as stream to console settings.console_stream = True @@ -49,12 +49,11 @@ if __name__ == "__main__": ``` poem = analyze("I really like when my dog does a trick!") - $ .................. + $ {"analysis": "A dog trick", "sentiment": true} - Add demo + SentimentAnalysis(analysis='A dog trick', sentiment=True) ``` -
## Instructions @@ -62,14 +61,15 @@ if __name__ == "__main__": !!! Step-by-Step **Necessary Imports** + ```python from funcchain import chain, settings from pydantic import BaseModel, Field - from rich import print ``` **Define the Data Model** Here, we define a `SentimentAnalysis` model with a description of the sentiment analysis and a boolean field indicating the sentiment. + ```python class SentimentAnalysis(BaseModel): analysis: str = Field(description="A description of the analysis") @@ -77,7 +77,9 @@ if __name__ == "__main__": ``` **Create the Analysis Function** + This 'analyze' function takes a string as input and is expected to return a `SentimentAnalysis` object by calling the `chain()` function from the `funcchain` library. + ```python def analyze(text: str) -> SentimentAnalysis: """ @@ -87,13 +89,14 @@ if __name__ == "__main__": ``` **Execution Configuration** + In the main block, configure the global settings to set the preferred LLM, enable console streaming, and run the `analyze` function with sample text. The result is printed using the `rich` library. + ```python - if __name__ == "__main__": - settings.llm = "ollama/wizardcoder:34b-python-q3_K_M" - settings.console_stream = True - poem = analyze("I really like when my dog does a trick!") - print(poem) + settings.llm = "ollama/openchat" + settings.console_stream = True + poem = analyze("I really like when my dog does a trick!") + print(poem) ``` !!!Important From 4743d6320cba0c12d1c5919b9e4ff0579315071d Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:44 +0400 Subject: [PATCH 413/451] =?UTF-8?q?=F0=9F=A7=B9=20Simplify=20sum=5Ffruits?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/openai_json_mode.md | 42 +++++++++++-------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/docs/features/openai_json_mode.md b/docs/features/openai_json_mode.md index c2bcd38..5eb1076 100644 --- a/docs/features/openai_json_mode.md +++ b/docs/features/openai_json_mode.md @@ -12,19 +12,14 @@

 ```python
-from funcchain import chain, settings
+from funcchain import chain
 from pydantic import BaseModel
 
-settings.console_stream = True
-
 class FruitSalad(BaseModel):
     bananas: int = 0
     apples: int = 0
 
-class Result(BaseModel):
-    sum: int
-
-def sum_fruits(fruit_salad: FruitSalad) -> Result:
+def sum_fruits(fruit_salad: FruitSalad) -> int:
     """
     Sum the number of fruits in a fruit salad.
     """
@@ -36,55 +31,46 @@ if __name__ == "__main__":
 ```
 
-Demo - -
-```python -fruit_salad = FruitSalad(bananas=3, apples=5) -assert sum_fruits(fruit_salad) == 8 -``` -
- Instructions !!! Step-by-Step **Necessary Imports** + `funcchain` for chaining functionality, and `pydantic` for the data models. + ```python from funcchain import chain, settings from pydantic import BaseModel ``` **Defining the Data Models** - We define two Pydantic models: `FruitSalad` with integer fields for the number of bananas and apples, and `Result`, which will hold the sum of the fruits. + + We define two Pydantic models: `FruitSalad` with integer fields for the number of bananas and apples. Of course feel free to change those classes according to your needs but use of `pydantic` is required. + ```python class FruitSalad(BaseModel): bananas: int = 0 apples: int = 0 - - class Result(BaseModel): - sum: int ``` - - **Summing Function** - The `sum_fruits` function is intended to take a `FruitSalad` object and use `chain()` for solving this task with an LLM. The result is returned as a `Result` object. + + The `sum_fruits` function is intended to take a `FruitSalad` object and use `chain()` for solving this task with an LLM. The result is returned then returned as integer. + ```python - def sum_fruits(fruit_salad: FruitSalad) -> Result: + def sum_fruits(fruit_salad: FruitSalad) -> int: """ Sum the number of fruits in a fruit salad. """ return chain() ``` - **Execution Block** + ```python - if __name__ == "__main__": - fruit_salad = FruitSalad(bananas=3, apples=5) - assert sum_fruits(fruit_salad) == 8 + fruit_salad = FruitSalad(bananas=3, apples=5) + assert sum_fruits(fruit_salad) == 8 ``` In the primary execution section of the script, we instantiate a `FruitSalad` object with predefined quantities of bananas and apples. We then verify that the `sum_fruits` function accurately calculates the total count of fruits, which should be 8 in this case. From b3baf52071831a338df0e88660a1cc10f25ccabf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 414/451] =?UTF-8?q?=F0=9F=94=81=20Rename=20retry=20parsing?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/retry_parsing.md | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/features/retry_parsing.md diff --git a/docs/features/retry_parsing.md b/docs/features/retry_parsing.md new file mode 100644 index 0000000..95f1f4a --- /dev/null +++ b/docs/features/retry_parsing.md @@ -0,0 +1,123 @@ + +# Retry Parsing + +!!! Example + [pydantic_validation.py](https://github.com/shroominic/funcchain/blob/main/examples/pydantic_validation.py) + + You can adapt this for your own usage. + This serves as an example of how to implement data validation and task creation using pydantic for data models and funcchain for processing natural language input. + + The main functionality is to parse a user description, validate the task details, and create a new Task object with unique keywords and a difficulty level within a specified range. + +## Full Code Example + +

+```python
+from funcchain import chain, settings
+from pydantic import BaseModel, field_validator
+
+# settings.llm = "ollama/openchat"
+settings.console_stream = True
+
+class Task(BaseModel):
+    name: str
+    difficulty: int
+    keywords: list[str]
+
+    @field_validator("keywords")
+    def keywords_must_be_unique(cls, v: list[str]) -> list[str]:
+        if len(v) != len(set(v)):
+            raise ValueError("keywords must be unique")
+        return v
+
+    @field_validator("difficulty")
+    def difficulty_must_be_between_1_and_10(cls, v: int) -> int:
+        if v < 10 or v > 100:
+            raise ValueError("difficulty must be between 10 and 100")
+        return v
+
+def gather_infos(user_description: str) -> Task:
+    """
+    Based on the user description,
+    create a new task to put on the todo list.
+    """
+    return chain()
+
+if __name__ == "__main__":
+    task = gather_infos("cleanup the kitchen")
+    print(f"{task=}")
+```
+
+ +Demo + +
+ ```python + User: + $ cleanup the kitchen + + task=Task + name='cleanup', + difficulty=30, + keywords=['kitchen', 'cleanup'] + ``` + +
+ +## Instructions + +!!! Step-by-Step + **Necessary Imports** + + ```python + from funcchain import chain, settings + from pydantic import BaseModel, field_validator + ``` + + **Define the Task Model with Validators** + The `Task` class is a Pydantic model with fields: `name`, `difficulty`, and `keywords`. Validators ensure data integrity: + + - `keywords_must_be_unique`: Checks that all keywords are distinct. + - `difficulty_must_be_between_1_and_10`: Ensures difficulty is within 10 to 100. + + ```python + class Task(BaseModel): + name: str # Task name. + difficulty: int # Difficulty level (10-100). + keywords: list[str] # Unique keywords. + + @field_validator("keywords") + def keywords_must_be_unique(cls, v: list[str]) -> list[str]: + # Ensure keyword uniqueness. + if len(v) != len(set(v)): + raise ValueError("keywords must be unique") + return v + + @field_validator("difficulty") + def difficulty_must_be_between_1_and_10(cls, v: int) -> int: + # Validate difficulty range. + if v < 10 or v > 100: + raise ValueError("difficulty must be between 10 and 100") + return v + ``` + + **Implement the Information Gathering Function** + The gather_infos function is designed to take a user description and use the chain function to process and validate the input, returning a new Task object. + Adjust the string description to match your purposes when changing the code above. + + ```python + def gather_infos(user_description: str) -> Task: + """ + Based on the user description, + create a new task to put on the todo list. + """ + return chain() + ``` + + **Execute the Script** + Runs gather_infos with a sample and prints the Task. + ```python + if __name__ == "__main__": + task = gather_infos("cleanup the kitchen") + print(f"{task=}") + ``` From 092211951837b6a6460a03766d5a1fd9844878ae Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 415/451] =?UTF-8?q?=F0=9F=9A=9A=20Refactor=20main=20execut?= =?UTF-8?q?ion=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/static_router.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/features/static_router.md b/docs/features/static_router.md index 753feef..f0865e2 100644 --- a/docs/features/static_router.md +++ b/docs/features/static_router.md @@ -49,10 +49,9 @@ class Router(BaseModel): def route_query(user_query: str) -> Router: return chain() -if __name__ == "__main__": - user_query = input("Enter your query: ") - routed_chain = route_query(user_query) - routed_chain.invoke_route(user_query) +user_query = input("Enter your query: ") +routed_chain = route_query(user_query) +routed_chain.invoke_route(user_query) ``` @@ -61,11 +60,10 @@ Demo
```python - User: - $ Enter your query: I need to process a CSV file + Enter your query: + $ I need to process a CSV file Handling CSV requests with user query: I need to process a CSV file - ```
@@ -84,7 +82,9 @@ Demo ``` **Define Route Handlers** + These functions are the specific handlers for different types of user queries. + ```python def handle_pdf_requests(user_query: str) -> None: print("Handling PDF requests with user query: ", user_query) @@ -97,6 +97,7 @@ Demo ``` **Create RouteChoices Enum and Router Model** + RouteChoices is an Enum that defines the possible routes. Router is a Pydantic model that selects and invokes the appropriate handler based on the route. ```python @@ -119,6 +120,7 @@ Demo ``` **Implement Routing Logic** + The route_query function is intended to determine the best route for a given user query using the `chain()` function. ```python @@ -127,6 +129,7 @@ Demo ``` **Execute the Routing System** + This block runs the routing system, asking the user for a query and then processing it through the defined routing logic. ```python From 0d67e13f44f87e7793c0dd877ac629fba151b4c2 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 416/451] =?UTF-8?q?=F0=9F=93=9D=20Improve=20code=20formatt?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/stream.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/features/stream.md b/docs/features/stream.md index a9b3412..c22ce37 100644 --- a/docs/features/stream.md +++ b/docs/features/stream.md @@ -29,10 +29,12 @@ with stream_to(print): Demo
-```python -$ ..... -$ Once upon a time in a galaxy far, far away, there was a space cat named Whiskertron... -``` + ```python + with stream_to(print): + generate_story_of("a space cat") + + $ Once upon a time in a galaxy far, far away, there was a space cat named Whiskertron... + ```
## Instructions @@ -40,19 +42,23 @@ $ Once upon a time in a galaxy far, far away, there was a space cat named Whiske !!! Step-by-Step **Necessary Imports** + ```python from funcchain import chain, settings from funcchain.backend.streaming import stream_to ``` **Configure Settings** + The settings are configured to set the temperature, which controls the creativity of the language model's output. Experiment with different values. + ```python settings.temperature = 1 ``` **Define the Story Generation Function** + The generate_story_of function is designed to take a topic and use the chain function to generate a story. ```python @@ -64,6 +70,7 @@ $ Once upon a time in a galaxy far, far away, there was a space cat named Whiske ``` **Execute the Streaming Generation** + This block uses the stream_to context manager to print the output of the story generation function as it is being streamed. This is how you stream the story while it is being generated. From 8f112847e02143b5fd79dc24bafcc763a73a3f4b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 417/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20example=20outpu?= =?UTF-8?q?t,=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/vision.md | 55 ++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/docs/features/vision.md b/docs/features/vision.md index 323b218..0fe57e0 100644 --- a/docs/features/vision.md +++ b/docs/features/vision.md @@ -16,7 +16,6 @@ from funcchain import Image, chain, settings from pydantic import BaseModel, Field settings.llm = "openai/gpt-4-vision-preview" -# settings.llm = "ollama/bakllava" settings.console_stream = True class AnalysisResult(BaseModel): @@ -33,29 +32,30 @@ def analyse_image(image: Image) -> AnalysisResult: """ return chain() -if __name__ == "__main__": - example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") +example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") - result = analyse_image(example_image) +result = analyse_image(example_image) - print("Theme:", result.theme) - print("Description:", result.description) - for obj in result.objects: - print("Found this object:", obj) +print("Theme:", result.theme) +print("Description:", result.description) +for obj in result.objects: + print("Found this object:", obj) ``` -Demo - -
-```python -Theme: Ancient Architecture -Description: An old Chinese temple with intricate designs. -Found this object: temple -Found this object: tree -Found this object: sky +Example Output + +```txt +Theme: Traditional Japanese architecture and nature during rainfall +Description: The image depicts a serene rainy scene with traditional Japanese buildings. The warm glow of lights from the windows contrasts with the cool tones of the rain. A cherry blossom tree in bloom adds a splash of color to the otherwise muted scene. Stone lanterns and stepping stones create a path leading to the building, while hanging lanterns with a skull motif suggest a cultural or festive significance. +Found this object: traditional Japanese building +Found this object: cherry blossom tree +Found this object: rain +Found this object: stepping stones +Found this object: stone lantern +Found this object: hanging lanterns with skull motif +Found this object: glowing windows ``` -
## Instructions @@ -63,19 +63,23 @@ Found this object: sky Oiur goal is the functionality is to analyze an image and extract its theme, a description, and a list of objects found within it. **Necessary Imports** + ```python from funcchain import Image, chain, settings from pydantic import BaseModel, Field ``` **Configure Settings** + The settings are configured to use a specific language model capable of image analysis and to enable console streaming for immediate output. + ```python settings.llm = "openai/gpt-4-vision-preview" settings.console_stream = True ``` **Define the AnalysisResult Model** + The AnalysisResult class models the expected output of the image analysis, including the theme, description, and objects detected in the image. ```python @@ -86,6 +90,7 @@ Found this object: sky ``` **Implement the Image Analysis Function** + The analyse_image function is designed to take an Image object and use the chain function to process the image and return an AnalysisResult object for later usage (here printing). ```python @@ -94,14 +99,14 @@ Found this object: sky ``` **Execute the Analysis** + This block runs the image analysis on an example image and prints the results when the script is executed directly. ```python - if __name__ == "__main__": - example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") - result = analyse_image(example_image) - print("Theme:", result.theme) - print("Description:", result.description) - for obj in result.objects: - print("Found this object:", obj) + example_image = Image.from_file("examples/assets/old_chinese_temple.jpg") + result = analyse_image(example_image) + print("Theme:", result.theme) + print("Description:", result.description) + for obj in result.objects: + print("Found this object:", obj) ``` From aecec5381ac18680a5255ce0db38f29d6577599b Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 418/451] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20up=20comments=20?= =?UTF-8?q?demos.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/demos.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/getting-started/demos.md b/docs/getting-started/demos.md index 37ebd15..df133e3 100644 --- a/docs/getting-started/demos.md +++ b/docs/getting-started/demos.md @@ -30,12 +30,10 @@ print(recipe.ingredients) !!! Step-by-Step ```python - # define your output shape class Recipe(BaseModel): ingredients: list[str] instructions: list[str] duration: int - ``` A Recipe class is defined, inheriting from BaseModel (pydantic library). This class @@ -44,12 +42,11 @@ print(recipe.ingredients) representing the duration ```python - # write prompts utilising all native python features def generate_recipe(topic: str) -> Recipe: """ Generate a recipe for a given topic. """ - return chain() # <- this is doing all the magic + return chain() ``` In this example the `generate_recipe` function takes a topic string and returns a `Recipe` instance for that topic. # Understanding chain() Functionality @@ -58,12 +55,10 @@ print(recipe.ingredients) The `chain()` function is the core component of funcchain. It takes the docstring, input arguments and return type of the function and compiles everything into a langchain runnable . It then executes the prompt with your input arguments if you call the function and returns the parsed result. - # Get your response + # Print your response ```python - # generate llm response recipe = generate_recipe("christmas dinner") - # recipe is automatically converted as pydantic model print(recipe.ingredients) ``` From eb406560716a03e5a2d194522c90c422f5c03933 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 419/451] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20Update=20poem=20ge?= =?UTF-8?q?neration=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/decorator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/decorator.py b/examples/decorator.py index aded07f..7a2cef9 100644 --- a/examples/decorator.py +++ b/examples/decorator.py @@ -7,7 +7,7 @@ @runnable def generate_poem(topic: str, context: str) -> str: """ - Generate a poem about the topic with the given context. + Generate a short poem about the topic with the given context. """ return chain() @@ -20,7 +20,7 @@ def generate_poem(topic: str, context: str) -> str: ], embedding=OpenAIEmbeddings(), ) -retriever = vectorstore.as_retriever() +retriever = vectorstore.as_retriever(search_kwargs={"k": 1}) retrieval_chain: Runnable = { "context": retriever, From 24d5789c8de593147748e8496b0a22d8e91e9761 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 420/451] =?UTF-8?q?=F0=9F=94=84=20Update=20model=20identif?= =?UTF-8?q?ier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/llamacpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llamacpp.py b/examples/llamacpp.py index 2a8cdfa..29c90c5 100644 --- a/examples/llamacpp.py +++ b/examples/llamacpp.py @@ -19,7 +19,7 @@ def analyze(text: str) -> SentimentAnalysis: if __name__ == "__main__": # set global llm - settings.llm = "llamacpp/nous-hermes-2-solar-10.7b" + settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" # log tokens as stream to console settings.console_stream = True From e1fd83ab241136bbdb1b91eb7dc2b55da06d9171 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 421/451] =?UTF-8?q?=F0=9F=94=84=20Update=20LLM=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/ollama.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ollama.py b/examples/ollama.py index 12606be..76328c8 100644 --- a/examples/ollama.py +++ b/examples/ollama.py @@ -19,7 +19,7 @@ def analyze(text: str) -> SentimentAnalysis: if __name__ == "__main__": # set global llm - settings.llm = "ollama/wizardcoder:34b-python-q3_K_M" + settings.llm = "ollama/openchat" # log tokens as stream to console settings.console_stream = True From ab9f294e8bb45de6c81473bdb172f012c2974f45 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 422/451] =?UTF-8?q?=F0=9F=8D=8E=20Simplify=20sum=5Ffruits?= =?UTF-8?q?=20return=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/openai_json_mode.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/openai_json_mode.py b/examples/openai_json_mode.py index ed4c1db..135887e 100644 --- a/examples/openai_json_mode.py +++ b/examples/openai_json_mode.py @@ -9,11 +9,7 @@ class FruitSalad(BaseModel): apples: int = 0 -class Result(BaseModel): - sum: int - - -def sum_fruits(fruit_salad: FruitSalad) -> Result: +def sum_fruits(fruit_salad: FruitSalad) -> int: """ Sum the number of fruits in a fruit salad. """ From 8d2adeb1c90ecddcc511de74f12739bd22c9f71f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 423/451] =?UTF-8?q?=F0=9F=93=9D=20Reorganize=20navigation?= =?UTF-8?q?=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0433917..93c5b68 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,26 +18,26 @@ nav: - "Input Args": "concepts/input.md" - "Prompting": "concepts/prompting.md" - "Output Parsing": "concepts/parser.md" - - "Pydantic Models": "concepts/pydantic.md" - "Errors": "concepts/errors.md" - "Langchain": "concepts/langchain.md" - - "Local Models": "concepts/local-models.md" - "Pydantic": "concepts/pydantic.md" + - "Local Models": "concepts/local-models.md" - "Streaming": "concepts/streaming.md" - "Unions": "concepts/unions.md" - "Vision": "concepts/vision.md" - - "Features": - - "ChatGPT": "features/chat.md" - - "Dynamic Router": "features/dynamic_router.md" - - "Enums": "features/enums.md" - - "Error Output": "features/error_output.md" + - "Examples": + # - "ChatGPT": "features/chat.md" - "Literals": "features/literals.md" + - "Retry Parsing": "features/retry_parsing.md" - "Structured vision output": "features/vision.md" + - "Enums": "features/enums.md" + - "Dynamic Router": "features/dynamic_router.md" - "Streaming Output": "features/stream.md" - - "Ollama (And other models)": "features/ollama.md" - - "Static Router": "features/static_router.md" - - "Pydantic Models": "features/pydantic_validation.md" + - "LlamaCpp": "features/llamacpp.md" - "OpenAI JSON Output": "features/openai_json_mode.md" + - "Static Router": "features/static_router.md" + - "Ollama": "features/ollama.md" + - "Error Output": "features/error_output.md" - "Advanced": - "Async": "advanced/async.md" - "Signature": "advanced/signature.md" @@ -49,7 +49,7 @@ nav: - "Contributing": - "Contributing": "contributing/dev-setup.md" - "Codebase Structure": "contributing/codebase-structure.md" - - "Code of Conduct": "contributing/code-of-conduct.md" + # - "Code of Conduct": "contributing/code-of-conduct.md" - "Contributors": "contributing/contributors.md" - "Security": "contributing/security.md" - "Roadmap": "contributing/roadmap.md" From 5812efc238c3cce0883627160dada475559636c4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 424/451] =?UTF-8?q?=F0=9F=A7=B9=20Simplify=20pyproject.tom?= =?UTF-8?q?l=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a608725..6fbb89b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,7 @@ name = "funcchain" version = "0.2.0-alpha.5" description = "🔖 write prompts as python functions" -authors = [ - { name = "Shroominic", email = "contact@shroominic.com" } -] +authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] dependencies = [ "langchain_openai>=0.0.3", "pydantic-settings>=2", @@ -15,7 +13,16 @@ dependencies = [ license = "MIT" readme = "README.md" requires-python = ">= 3.10, <3.13" -keywords = ["funcchain", "ai", "llm", "langchain", "pydantic", "pythonic", "cognitive systems", "agent framework"] +keywords = [ + "funcchain", + "ai", + "llm", + "langchain", + "pydantic", + "pythonic", + "cognitive systems", + "agent framework", +] classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", @@ -47,29 +54,17 @@ dev-dependencies = [ ] [project.optional-dependencies] -openai = [ - "langchain_openai", -] -ollama = [ - "langchain_community", -] -llamacpp = [ - "llama-cpp-python>=0.2.32", - "huggingface_hub>=0.20", -] -pillow = [ - "pillow", -] -example-extras = [ - "langchain>=0.1", - "faiss-cpu>=1.7.4", - "beautifulsoup4>=4.12", -] +openai = ["langchain_openai"] +ollama = ["langchain_community"] +llamacpp = ["llama-cpp-python>=0.2.32", "huggingface_hub>=0.20"] +pillow = ["pillow"] +example-extras = ["langchain>=0.1", "faiss-cpu>=1.7.4", "beautifulsoup4>=4.12"] all = [ "funcchain[pillow]", "funcchain[openai]", "funcchain[ollama]", "funcchain[llamacpp]", + "funcchain[example-extras]", "langchain", ] From bc206e7e69b0862a17d16647d64f207fddca6815 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 425/451] =?UTF-8?q?=F0=9F=93=A6=20Update=20development=20d?= =?UTF-8?q?ependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.lock | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 165c5dc..4d49e75 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,6 +12,7 @@ aiosignal==1.3.1 annotated-types==0.6.0 anyio==4.2.0 asttokens==2.4.1 +async-timeout==4.0.3 attrs==23.2.0 babel==2.14.0 certifi==2023.11.17 @@ -25,12 +26,13 @@ diskcache==5.6.3 distlib==0.3.8 distro==1.9.0 docstring-parser==0.15 +exceptiongroup==1.2.0 executing==2.0.1 +faiss-cpu==1.7.4 filelock==3.13.1 frozenlist==1.4.1 fsspec==2023.12.2 ghp-import==2.1.0 -greenlet==3.0.3 h11==0.14.0 httpcore==1.0.2 httpx==0.26.0 @@ -70,11 +72,13 @@ packaging==23.2 paginate==0.5.6 parso==0.8.3 pathspec==0.12.1 +pexpect==4.9.0 pillow==10.2.0 platformdirs==4.1.0 pluggy==1.4.0 pre-commit==3.6.0 prompt-toolkit==3.0.43 +ptyprocess==0.7.0 pure-eval==0.2.2 pydantic==2.5.3 pydantic-core==2.14.6 @@ -96,6 +100,7 @@ sqlalchemy==2.0.25 stack-data==0.6.3 tenacity==8.2.3 tiktoken==0.5.2 +tomli==2.0.1 tqdm==4.66.1 traitlets==5.14.1 types-pyyaml==6.0.12.12 From f3e9e9060ffa39b002f8cc1fd51386525ae3df46 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 426/451] =?UTF-8?q?=F0=9F=94=92=20Update=20requirements.lo?= =?UTF-8?q?ck=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.lock b/requirements.lock index 5dc3d0a..b6bbf99 100644 --- a/requirements.lock +++ b/requirements.lock @@ -11,9 +11,9 @@ annotated-types==0.6.0 anyio==4.2.0 certifi==2023.11.17 charset-normalizer==3.3.2 -colorama==0.4.6 distro==1.9.0 docstring-parser==0.15 +exceptiongroup==1.2.0 h11==0.14.0 httpcore==1.0.2 httpx==0.26.0 From 8808a474b13570144308bd1b1241d4d62072edc5 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 427/451] =?UTF-8?q?=E2=9C=A8=20Add=20runnable=20decorator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/funcchain/__init__.py b/src/funcchain/__init__.py index 86c6cd5..e53da3c 100644 --- a/src/funcchain/__init__.py +++ b/src/funcchain/__init__.py @@ -1,6 +1,7 @@ from pydantic import BaseModel from .backend.settings import settings +from .syntax.decorators import runnable from .syntax.executable import achain, chain from .syntax.input_types import Image from .syntax.output_types import Error @@ -9,6 +10,7 @@ "settings", "chain", "achain", + "runnable", "BaseModel", "Image", "Error", From a826fbca39f4d61c5367d2469be1521420618488 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:45 +0400 Subject: [PATCH 428/451] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Rename=20Pydantic?= =?UTF-8?q?=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/parser/primitive_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funcchain/parser/primitive_types.py b/src/funcchain/parser/primitive_types.py index 58b8729..7757b2f 100644 --- a/src/funcchain/parser/primitive_types.py +++ b/src/funcchain/parser/primitive_types.py @@ -24,7 +24,7 @@ def __init__( retry_llm: BaseChatModel | str | None = None, ) -> None: super().__init__( - pydantic_object=create_model("ExtractPrimitiveType", value=(primitive_type, ...)), + pydantic_object=create_model("Extract", value=(primitive_type, ...)), retry=retry, retry_llm=retry_llm, ) From b8b2bb2ea3cff450bc36dbb7318f022e8d37ec81 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:14:55 +0400 Subject: [PATCH 429/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20pydant?= =?UTF-8?q?ic=5Fvalidation.md=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/pydantic_validation.md | 123 --------------------------- 1 file changed, 123 deletions(-) delete mode 100644 docs/features/pydantic_validation.md diff --git a/docs/features/pydantic_validation.md b/docs/features/pydantic_validation.md deleted file mode 100644 index f1d2458..0000000 --- a/docs/features/pydantic_validation.md +++ /dev/null @@ -1,123 +0,0 @@ - -# Task Creation with Validated Fields - -!!! Example - [pydantic_validation.py](https://github.com/shroominic/funcchain/blob/main/examples/pydantic_validation.py) - - You can adapt this for your own usage. - This serves as an example of how to implement data validation and task creation using pydantic for data models and funcchain for processing natural language input. - - The main functionality is to parse a user description, validate the task details, and create a new Task object with unique keywords and a difficulty level within a specified range. - -## Full Code Example - -

-```python
-from funcchain import chain, settings
-from pydantic import BaseModel, field_validator
-
-# settings.llm = "ollama/openchat"
-settings.console_stream = True
-
-class Task(BaseModel):
-    name: str
-    difficulty: int
-    keywords: list[str]
-
-    @field_validator("keywords")
-    def keywords_must_be_unique(cls, v: list[str]) -> list[str]:
-        if len(v) != len(set(v)):
-            raise ValueError("keywords must be unique")
-        return v
-
-    @field_validator("difficulty")
-    def difficulty_must_be_between_1_and_10(cls, v: int) -> int:
-        if v < 10 or v > 100:
-            raise ValueError("difficulty must be between 10 and 100")
-        return v
-
-def gather_infos(user_description: str) -> Task:
-    """
-    Based on the user description,
-    create a new task to put on the todo list.
-    """
-    return chain()
-
-if __name__ == "__main__":
-    task = gather_infos("cleanup the kitchen")
-    print(f"{task=}")
-```
-
- -Demo - -
- ```python - User: - $ cleanup the kitchen - - task=Task - name='cleanup', - difficulty=30, - keywords=['kitchen', 'cleanup'] - ``` - -
- -## Instructions - -!!! Step-by-Step - **Necessary Imports** - - ```python - from funcchain import chain, settings - from pydantic import BaseModel, field_validator - ``` - - **Define the Task Model with Validators** - The `Task` class is a Pydantic model with fields: `name`, `difficulty`, and `keywords`. Validators ensure data integrity: - - - `keywords_must_be_unique`: Checks that all keywords are distinct. - - `difficulty_must_be_between_1_and_10`: Ensures difficulty is within 10 to 100. - - ```python - class Task(BaseModel): - name: str # Task name. - difficulty: int # Difficulty level (10-100). - keywords: list[str] # Unique keywords. - - @field_validator("keywords") - def keywords_must_be_unique(cls, v: list[str]) -> list[str]: - # Ensure keyword uniqueness. - if len(v) != len(set(v)): - raise ValueError("keywords must be unique") - return v - - @field_validator("difficulty") - def difficulty_must_be_between_1_and_10(cls, v: int) -> int: - # Validate difficulty range. - if v < 10 or v > 100: - raise ValueError("difficulty must be between 10 and 100") - return v - ``` - - **Implement the Information Gathering Function** - The gather_infos function is designed to take a user description and use the chain function to process and validate the input, returning a new Task object. - Adjust the string description to match your purposes when changing the code above. - - ```python - def gather_infos(user_description: str) -> Task: - """ - Based on the user description, - create a new task to put on the todo list. - """ - return chain() - ``` - - **Execute the Script** - Runs gather_infos with a sample and prints the Task. - ```python - if __name__ == "__main__": - task = gather_infos("cleanup the kitchen") - print(f"{task=}") - ``` From a12d71d3c27646605eec0505e10fcc69e4072fbf Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 430/451] =?UTF-8?q?=E2=9C=A8=20Add=20async=20programming?= =?UTF-8?q?=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/async.md | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/advanced/async.md b/docs/advanced/async.md index a26eee2..c24a23d 100644 --- a/docs/advanced/async.md +++ b/docs/advanced/async.md @@ -2,24 +2,38 @@ ## Why and how to use using async? -### TODO +Asyncronous promgramming is a way to easily parallelize processes in python. +This is very useful when dealing with LLMs because every request takes a long time and the python interpreter should do alot of other things in the meantime instead of waiting for the request. -## How to use async? +Checkout [this brillian async tutorial](https://fastapi.tiangolo.com/async/) if you never coded in an asyncronous way. -### TODO +## Async in FuncChain -## Async Examples +You can use async in funcchain by creating your functions using `achain()` instead of the normal `chain()`. +It would then look like this: -### TODO +```python +from funcchain import achain -## Async in FuncChain +async def generate_poem(topic: str) -> str: + """ + Generate a poem inspired by the given topic. + """ + return await achain() +``` -### TODO +You can then `await` the async `generate_poem` function inside another async funtion or directly call it using `asyncio.run(generate_poem("birds"))`. ## Async in LangChain -### TODO +When converting your funcchains into a langchain runnable you can use the native langchain way of async. +This would be `.ainvoke(...)`, `.astream(...)` or `.abatch(...)` . ## Async Streaming -### TODO +You can use langchains async streaming interface but also use the `stream_to(...)` wrapper (explained [here](../concepts/streaming.md#strem_to-wrapper)) as an async context manager. + +```python +async with stream_to(...): + await ... +``` From 4bb13b8098bb4bc3694b89f742a82ce3444606f6 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 431/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20trailing=20whit?= =?UTF-8?q?espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/concepts/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/input.md b/docs/concepts/input.md index ad2a075..bc3b39d 100644 --- a/docs/concepts/input.md +++ b/docs/concepts/input.md @@ -77,7 +77,7 @@ and then added to the prompt. USER: full_name='Herbert Geier' email='hello@bert.com' - + Create a creative username from the given user. ``` From 063fc44b587c5e633d2bd445167f81e34eb42e59 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 432/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20extraneous=20wh?= =?UTF-8?q?itespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/dynamic_router.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/features/dynamic_router.md b/docs/features/dynamic_router.md index 7abf60c..5bdc421 100644 --- a/docs/features/dynamic_router.md +++ b/docs/features/dynamic_router.md @@ -131,7 +131,7 @@ router.invoke_route("Can you summarize this csv?") **Implement Route Representation** Establish a Router class - + ```python class DynamicChatRouter(BaseModel): routes: dict[str, Route] @@ -147,7 +147,7 @@ router.invoke_route("Can you summarize this csv?") ``` **invoke_route(user_query: str, **kwargs: Any) -> Any: ** - + This method takes a user query and additional keyword arguments. Inside invoke_route, an Enum named RouteChoices is dynamically created with keys corresponding to the route names. This Enum is used to validate the selected route. ```python @@ -200,7 +200,7 @@ router.invoke_route("Can you summarize this csv?") Now you can use the structured output to execute programatically based on a natural language input. Establish functions tailored to your needs. - + ```python def handle_pdf_requests(user_query: str) -> str: return "Handling PDF requests with user query: " + user_query @@ -211,7 +211,7 @@ router.invoke_route("Can you summarize this csv?") def handle_default_requests(user_query: str) -> str: return "Handling DEFAULT requests with user query: " + user_query ``` - + **Define the routes** And bind the previous established functions. @@ -236,7 +236,7 @@ router.invoke_route("Can you summarize this csv?") ``` **Get output** - + Use the router.invoke_route method to process the user query and obtain the appropriate response. ```python From cdcf8133221718dbdc899772f1b3dd4faad23bba Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 433/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20trailing=20whit?= =?UTF-8?q?espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/error_output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/error_output.md b/docs/features/error_output.md index bd12030..1c98102 100644 --- a/docs/features/error_output.md +++ b/docs/features/error_output.md @@ -82,7 +82,7 @@ Demo **Implement the Extraction Function** The `extract_user_info` function is intended to process the input text and return either a User object with extracted information or an Error if the information is not sufficient. - + ```python def extract_user_info(text: str) -> User | Error: """ From 0314bdce57dce2bdb22a7742b4c136b7970619b7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 434/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20trailing=20whit?= =?UTF-8?q?espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/literals.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/literals.md b/docs/features/literals.md index b31caac..c7d372d 100644 --- a/docs/features/literals.md +++ b/docs/features/literals.md @@ -49,7 +49,7 @@ Demo !!! Step-by-Step **Necessary Imports** - + ```python from typing import Literal from funcchain import chain @@ -85,7 +85,7 @@ Demo **Execute the Ranking System** This block is used to execute the ranking function and print the results when the script is run directly. - + ```python rank = rank_output("The quick brown fox jumps over the lazy dog.") print(rank) From 75eb081a145d16a1f60e471e7bcb25e99b282320 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 435/451] =?UTF-8?q?=F0=9F=93=9D=20Add=20llamacpp=20usage?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/llamacpp.md | 111 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/features/llamacpp.md diff --git a/docs/features/llamacpp.md b/docs/features/llamacpp.md new file mode 100644 index 0000000..2309295 --- /dev/null +++ b/docs/features/llamacpp.md @@ -0,0 +1,111 @@ + +# Different LLMs with funcchain EASY TO USE + +!!! Example + See [llamacpp.py](https://github.com/shroominic/funcchain/blob/main/examples/llamacpp.py) + Also see supported [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) + + In this example, we will use the funcchain library to perform sentiment analysis on a piece of text. This showcases how funcchain can seamlessly utilize different Language Models (LLMs) using local llamacpp models, without many code changes.. + + This is particularly useful for developers looking to integrate different models in a single application or just experimenting with different models. + +## Full Code Example + +

+```python
+from funcchain import chain, settings
+from pydantic import BaseModel, Field
+from rich import print
+
+# define your model
+class SentimentAnalysis(BaseModel):
+    analysis: str = Field(description="A description of the analysis")
+    sentiment: bool = Field(description="True for Happy, False for Sad")
+
+# define your prompt
+def analyze(text: str) -> SentimentAnalysis:
+    """
+    Determines the sentiment of the text.
+    """
+    return chain()
+
+# set global llm
+settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M"
+
+# log tokens as stream to console
+settings.console_stream = True
+
+# run prompt
+poem = analyze("I really like when my dog does a trick!")
+
+# show final parsed output
+print(poem)
+```
+
+ +# Demo + +
+ ``` + poem = analyze("I really like when my dog does a trick!") + + $ {"analysis": "A dog trick", "sentiment": true} + + SentimentAnalysis(analysis='A dog trick', sentiment=True) + + ``` +
+ +## Instructions + +!!! Step-by-Step + + **Necessary Imports** + + ```python + from funcchain import chain, settings + from pydantic import BaseModel, Field + ``` + + **Define the Data Model** + + Here, we define a `SentimentAnalysis` model with a description of the sentiment analysis and a boolean field indicating the sentiment. + + ```python + class SentimentAnalysis(BaseModel): + analysis: str = Field(description="A description of the analysis") + sentiment: bool = Field(description="True for Happy, False for Sad") + ``` + + **Create the Analysis Function** + + This 'analyze' function takes a string as input and is expected to return a `SentimentAnalysis` object by calling the `chain()` function from the `funcchain` library. + + ```python + def analyze(text: str) -> SentimentAnalysis: + """ + Determines the sentiment of the text. + """ + return chain() + ``` + + **Execution Configuration** + + In the main block, configure the global settings to set the preferred LLM, enable console streaming, and run the `analyze` function with sample text. + + ```python + # set global llm + settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" + + # log tokens as stream to console + settings.console_stream = True + + # run prompt + poem = analyze("I really like when my dog does a trick!") + + # show final parsed output + print(poem) + ``` + + !!!Important + We need to note here is that `settings.llm` can be adjusted to any model mentioned in [MODELS.md](https://github.com/shroominic/funcchain/blob/main/MODELS.md) and your funcchain code will still work and `chain()` does everything in the background for you. From b0155352087757256ad0e5cfb04db834cbbaf3d8 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 436/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20redundant=20whi?= =?UTF-8?q?tespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/ollama.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/ollama.md b/docs/features/ollama.md index c1ba902..f19829a 100644 --- a/docs/features/ollama.md +++ b/docs/features/ollama.md @@ -79,7 +79,7 @@ if __name__ == "__main__": **Create the Analysis Function** This 'analyze' function takes a string as input and is expected to return a `SentimentAnalysis` object by calling the `chain()` function from the `funcchain` library. - + ```python def analyze(text: str) -> SentimentAnalysis: """ @@ -89,9 +89,9 @@ if __name__ == "__main__": ``` **Execution Configuration** - - In the main block, configure the global settings to set the preferred LLM, enable console streaming, and run the `analyze` function with sample text. The result is printed using the `rich` library. - + + In the main block, configure the global settings to set the preferred LLM, enable console streaming, and run the `analyze` function with sample text. + ```python settings.llm = "ollama/openchat" settings.console_stream = True From 630eb4af0e26535655f56ba9258fe78e8a3c65f3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 437/451] =?UTF-8?q?=F0=9F=93=9D=20Update=20whitespace=20co?= =?UTF-8?q?nsistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/openai_json_mode.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/features/openai_json_mode.md b/docs/features/openai_json_mode.md index 5eb1076..4cd6132 100644 --- a/docs/features/openai_json_mode.md +++ b/docs/features/openai_json_mode.md @@ -37,17 +37,17 @@ Instructions **Necessary Imports** `funcchain` for chaining functionality, and `pydantic` for the data models. - + ```python from funcchain import chain, settings from pydantic import BaseModel ``` **Defining the Data Models** - + We define two Pydantic models: `FruitSalad` with integer fields for the number of bananas and apples. Of course feel free to change those classes according to your needs but use of `pydantic` is required. - + ```python class FruitSalad(BaseModel): bananas: int = 0 @@ -55,9 +55,9 @@ Instructions ``` **Summing Function** - + The `sum_fruits` function is intended to take a `FruitSalad` object and use `chain()` for solving this task with an LLM. The result is returned then returned as integer. - + ```python def sum_fruits(fruit_salad: FruitSalad) -> int: """ @@ -67,7 +67,7 @@ Instructions ``` **Execution Block** - + ```python fruit_salad = FruitSalad(bananas=3, apples=5) assert sum_fruits(fruit_salad) == 8 From 45b676c95c7b582eb2392953fc7810ef2d9fc74c Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 438/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20extra=20whitesp?= =?UTF-8?q?aces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/static_router.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/features/static_router.md b/docs/features/static_router.md index f0865e2..b46037d 100644 --- a/docs/features/static_router.md +++ b/docs/features/static_router.md @@ -73,7 +73,7 @@ Demo We will implement a script with the functionality to take a user query, determine the type of request (PDF, CSV, or default), and invoke the appropriate handler function. **Necessary Imports** - + ```python from enum import Enum from typing import Any @@ -99,7 +99,7 @@ Demo **Create RouteChoices Enum and Router Model** RouteChoices is an Enum that defines the possible routes. Router is a Pydantic model that selects and invokes the appropriate handler based on the route. - + ```python class RouteChoices(str, Enum): pdf = "pdf" @@ -122,16 +122,16 @@ Demo **Implement Routing Logic** The route_query function is intended to determine the best route for a given user query using the `chain()` function. - + ```python def route_query(user_query: str) -> Router: return chain() ``` **Execute the Routing System** - + This block runs the routing system, asking the user for a query and then processing it through the defined routing logic. - + ```python user_query = input("Enter your query: ") routed_chain = route_query(user_query) From 242329ed037aa553c908c902a57b85ff76c20b78 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 439/451] =?UTF-8?q?=F0=9F=94=A4=20Remove=20trailing=20whit?= =?UTF-8?q?espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/stream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/stream.md b/docs/features/stream.md index c22ce37..bdaa4dd 100644 --- a/docs/features/stream.md +++ b/docs/features/stream.md @@ -70,7 +70,7 @@ Demo ``` **Execute the Streaming Generation** - + This block uses the stream_to context manager to print the output of the story generation function as it is being streamed. This is how you stream the story while it is being generated. From 807fbed026679f35da1ee1f479625d5d65ce1a5f Mon Sep 17 00:00:00 2001 From: Shroominic Date: Thu, 1 Feb 2024 14:45:35 +0400 Subject: [PATCH 440/451] =?UTF-8?q?=F0=9F=93=9D=20Remove=20extra=20whitesp?= =?UTF-8?q?ace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/getting-started/demos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/demos.md b/docs/getting-started/demos.md index df133e3..b49081f 100644 --- a/docs/getting-started/demos.md +++ b/docs/getting-started/demos.md @@ -111,7 +111,7 @@ print(recipe.ingredients) The Field descriptions serve as prompts for the language model to understand the data structure. Additionally you can include a docstring for each model class to provide further information to the LLM. - + !!! Important Everything including class names, argument names, doc string and field descriptions are part of the prompt and can be optimised using prompting techniques. From 83a865c61ee35b2eaa16f705a777e967b70ff582 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 13:59:59 +0400 Subject: [PATCH 441/451] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20extra=20whitesp?= =?UTF-8?q?ace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/features/llamacpp.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/llamacpp.md b/docs/features/llamacpp.md index 2309295..b9969f9 100644 --- a/docs/features/llamacpp.md +++ b/docs/features/llamacpp.md @@ -80,7 +80,7 @@ print(poem) **Create the Analysis Function** This 'analyze' function takes a string as input and is expected to return a `SentimentAnalysis` object by calling the `chain()` function from the `funcchain` library. - + ```python def analyze(text: str) -> SentimentAnalysis: """ @@ -90,9 +90,9 @@ print(poem) ``` **Execution Configuration** - + In the main block, configure the global settings to set the preferred LLM, enable console streaming, and run the `analyze` function with sample text. - + ```python # set global llm settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" From 38dd45f1dde2953aa0d8a178ceb294501a97fd40 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 13:59:59 +0400 Subject: [PATCH 442/451] =?UTF-8?q?=F0=9F=94=A7=20Remove=20LLM=20setting,?= =?UTF-8?q?=20add=20assert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/async/expert_answer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/async/expert_answer.py b/examples/async/expert_answer.py index 612debf..faa7852 100644 --- a/examples/async/expert_answer.py +++ b/examples/async/expert_answer.py @@ -6,7 +6,6 @@ from pydantic import BaseModel settings.temperature = 1 -settings.llm = "openai/gpt-3.5-turbo-1106" async def generate_answer(question: str) -> str: @@ -46,3 +45,5 @@ async def expert_answer(question: str) -> str: answer = _await(expert_answer(question)) print(answer) + + assert isinstance(answer, str) From 397e29f4771b6c2ab011f62ee535056f23318860 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 13:59:59 +0400 Subject: [PATCH 443/451] =?UTF-8?q?=E2=9C=85=20Add=20type=20assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/async/startup_names.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/async/startup_names.py b/examples/async/startup_names.py index d2e8bdd..6f5f2c3 100644 --- a/examples/async/startup_names.py +++ b/examples/async/startup_names.py @@ -30,4 +30,6 @@ async def generate_random_startups(topic: str, amount: int = 3) -> list[StartupC for startup in startups: print("name:", startup.name) + assert isinstance(startup.name, str) print("concept:", startup.description) + assert isinstance(startup.description, str) From a4da047fcb4604a2b027ff80b3392a9775150148 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 13:59:59 +0400 Subject: [PATCH 444/451] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20dynami?= =?UTF-8?q?c=5Fmodel=5Fgeneration.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../experiments/dynamic_model_generation.py | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 examples/experiments/dynamic_model_generation.py diff --git a/examples/experiments/dynamic_model_generation.py b/examples/experiments/dynamic_model_generation.py deleted file mode 100644 index 1da1a5b..0000000 --- a/examples/experiments/dynamic_model_generation.py +++ /dev/null @@ -1,57 +0,0 @@ -from funcchain import chain, settings -from funcchain.syntax.output_types import CodeBlock -from langchain_community.document_loaders import WebBaseLoader -from pydantic import BaseModel -from rich import print - -settings.llm = "gpt-4-1106-preview" -settings.context_lenght = 4096 * 8 - - -def create_model(web_page: str) -> CodeBlock: - """ - Based on the pure web page, create a Pydantic to extract the core contents of the page. - Create now a Pydantic model to represent this structure. - Only include imports and the model class. - Always name the class "StructuredOutput". The user can change it later. - """ - return chain() - - -def fix_imports(error: str) -> CodeBlock: - """ - Write proper import statements for the given error. - """ - return chain() - - -if __name__ == "__main__": - url = input("Give me a link and I scrape your page!\n> Url: ") - - page = WebBaseLoader(url).load() - - model = create_model(page.__str__()) - - print("Model:\n", model.code) - - try: - exec(model.code) - except Exception as e: - imports = fix_imports(str(e)) - exec(imports.code) - exec(model.code) - - class StructuredOutput(BaseModel): - ... - - def scrape_page( - page: str, - ) -> StructuredOutput: - """ - Scrape the unstructured data into the given shape. - """ - return chain() - - output = scrape_page(str(page)) - - print(output) From 70df0b1e5352a4bc16c4d1e141f789cdefa6a958 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 13:59:59 +0400 Subject: [PATCH 445/451] =?UTF-8?q?=F0=9F=94=84=20Update=20model=20referen?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/llamacpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llamacpp.py b/examples/llamacpp.py index 29c90c5..4b7e796 100644 --- a/examples/llamacpp.py +++ b/examples/llamacpp.py @@ -19,7 +19,7 @@ def analyze(text: str) -> SentimentAnalysis: if __name__ == "__main__": # set global llm - settings.llm = "llamacpp/openchat-3.5-0106:Q3_K_M" + settings.llm = "llamacpp/Nous-Hermes-2-SOLAR-10.7B" # log tokens as stream to console settings.console_stream = True From 3eb7da83ce0035424e0971d969c4ea69ad50cfff Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 13:59:59 +0400 Subject: [PATCH 446/451] =?UTF-8?q?=F0=9F=93=A5=20Improve=20model=20downlo?= =?UTF-8?q?ad=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/funcchain/model/defaults.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/funcchain/model/defaults.py b/src/funcchain/model/defaults.py index 1f82c41..046d593 100644 --- a/src/funcchain/model/defaults.py +++ b/src/funcchain/model/defaults.py @@ -33,26 +33,22 @@ def get_gguf_model( if (p := model_path / f"{name.lower()}.{label}.gguf").exists(): return p - # check if available on huggingface - try: - # check local cache + repo_id = f"TheBloke/{name}-GGUF" + filename = f"{name.lower()}.{label}.gguf" - input( - f"Do you want to download this model from huggingface.co/TheBloke/{name}-GGUF ?\n" - "Press enter to continue." - ) + try: + # todo make setting to turn prints off print("\033c") - print("Downloading model from huggingface...") + print("Downloading model from huggingface... (Ctrl+C to cancel)") p = hf_hub_download( - repo_id=f"TheBloke/{name}-GGUF", - filename=f"{name.lower()}.{label}.gguf", + repo_id, + filename, local_dir=model_path, local_dir_use_symlinks=True, ) print("\033c") return Path(p) - except Exception as e: - print(e) + except Exception: raise ValueError(f"ModelNotFound: {name}.{label}") From 5e1f2f4ad51bb24f32d152971f04b5d86cd65e39 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 14:07:42 +0400 Subject: [PATCH 447/451] =?UTF-8?q?=F0=9F=94=A7=20Update=20Python=20versio?= =?UTF-8?q?ns,=20indentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/code-check.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 1ac760d..246f097 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -6,21 +6,21 @@ jobs: pre-commit: strategy: matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ["3.10", "3.11"] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: eifinger/setup-rye@v1 - with: - enable-cache: true - cache-prefix: 'venv-funcchain' - - name: pin version - run: rye pin ${{ matrix.python-version }} - - name: Sync rye - run: rye sync - - name: Run pre-commit - run: rye run pre-commit run --all-files - - name: Run tests - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: rye run pytest -m "not skip_on_actions" + - uses: actions/checkout@v2 + - uses: eifinger/setup-rye@v1 + with: + enable-cache: true + cache-prefix: "venv-funcchain" + - name: pin version + run: rye pin ${{ matrix.python-version }} + - name: Sync rye + run: rye sync + - name: Run pre-commit + run: rye run pre-commit run --all-files + - name: Run tests + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: rye run pytest -m "not skip_on_actions" From 00d80c02f473d089961849ba87f87fa7dd83c6d7 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 14:07:42 +0400 Subject: [PATCH 448/451] =?UTF-8?q?=F0=9F=94=A7=20Ignore=20.python-version?= =?UTF-8?q?=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8cb8a54..e326d92 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,4 @@ cython_debug/ vscext .models +.python-version From ddf190af6e689611cd56da9d4fd0f531b0c4eea4 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 14:07:42 +0400 Subject: [PATCH 449/451] =?UTF-8?q?=F0=9F=94=92=20Update=20dependency=20ve?= =?UTF-8?q?rsions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-dev.lock | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 4d49e75..7d9f3cd 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -5,28 +5,28 @@ # pre: false # features: [] # all-features: false +# with-sources: false -e file:. -aiohttp==3.9.1 +aiohttp==3.9.3 aiosignal==1.3.1 annotated-types==0.6.0 anyio==4.2.0 asttokens==2.4.1 -async-timeout==4.0.3 attrs==23.2.0 babel==2.14.0 -certifi==2023.11.17 +beautifulsoup4==4.12.3 +certifi==2024.2.2 cfgv==3.4.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 -dataclasses-json==0.6.3 +dataclasses-json==0.6.4 decorator==5.1.1 diskcache==5.6.3 distlib==0.3.8 distro==1.9.0 docstring-parser==0.15 -exceptiongroup==1.2.0 executing==2.0.1 faiss-cpu==1.7.4 filelock==3.13.1 @@ -40,18 +40,18 @@ huggingface-hub==0.20.3 identify==2.5.33 idna==3.6 iniconfig==2.0.0 -ipython==8.20.0 +ipython==8.21.0 isort==5.13.2 jedi==0.19.1 jinja2==3.1.3 jsonpatch==1.33 jsonpointer==2.4 -langchain==0.1.3 -langchain-community==0.0.15 -langchain-core==0.1.15 -langchain-openai==0.0.3 -langsmith==0.0.83 -llama-cpp-python==0.2.32 +langchain==0.1.5 +langchain-community==0.0.17 +langchain-core==0.1.18 +langchain-openai==0.0.5 +langsmith==0.0.86 +llama-cpp-python==0.2.38 markdown==3.5.2 markdown-it-py==3.0.0 markupsafe==2.1.4 @@ -60,32 +60,32 @@ matplotlib-inline==0.1.6 mdurl==0.1.2 mergedeep==1.3.4 mkdocs==1.5.3 -mkdocs-material==9.5.5 +mkdocs-material==9.5.6 mkdocs-material-extensions==1.3.1 -multidict==6.0.4 +multidict==6.0.5 mypy==1.8.0 mypy-extensions==1.0.0 nodeenv==1.8.0 numpy==1.26.3 -openai==1.9.0 +openai==1.10.0 packaging==23.2 paginate==0.5.6 parso==0.8.3 pathspec==0.12.1 pexpect==4.9.0 pillow==10.2.0 -platformdirs==4.1.0 +platformdirs==4.2.0 pluggy==1.4.0 pre-commit==3.6.0 prompt-toolkit==3.0.43 ptyprocess==0.7.0 pure-eval==0.2.2 -pydantic==2.5.3 -pydantic-core==2.14.6 +pydantic==2.6.0 +pydantic-core==2.16.1 pydantic-settings==2.1.0 pygments==2.17.2 pymdown-extensions==10.7 -pytest==7.4.4 +pytest==8.0.0 python-dateutil==2.8.2 python-dotenv==1.0.1 pyyaml==6.0.1 @@ -93,20 +93,20 @@ pyyaml-env-tag==0.1 regex==2023.12.25 requests==2.31.0 rich==13.7.0 -ruff==0.1.14 +ruff==0.2.0 six==1.16.0 sniffio==1.3.0 +soupsieve==2.5 sqlalchemy==2.0.25 stack-data==0.6.3 tenacity==8.2.3 tiktoken==0.5.2 -tomli==2.0.1 tqdm==4.66.1 traitlets==5.14.1 types-pyyaml==6.0.12.12 typing-extensions==4.9.0 typing-inspect==0.9.0 -urllib3==2.1.0 +urllib3==2.2.0 virtualenv==20.25.0 watchdog==3.0.0 wcwidth==0.2.13 From c2693c76012b593fbb36fead239642f614757780 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 14:07:42 +0400 Subject: [PATCH 450/451] =?UTF-8?q?=F0=9F=94=92=20Update=20dependencies=20?= =?UTF-8?q?versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.lock b/requirements.lock index b6bbf99..57cdc12 100644 --- a/requirements.lock +++ b/requirements.lock @@ -5,15 +5,15 @@ # pre: false # features: [] # all-features: false +# with-sources: false -e file:. annotated-types==0.6.0 anyio==4.2.0 -certifi==2023.11.17 +certifi==2024.2.2 charset-normalizer==3.3.2 distro==1.9.0 docstring-parser==0.15 -exceptiongroup==1.2.0 h11==0.14.0 httpcore==1.0.2 httpx==0.26.0 @@ -21,17 +21,17 @@ idna==3.6 jinja2==3.1.3 jsonpatch==1.33 jsonpointer==2.4 -langchain-core==0.1.15 -langchain-openai==0.0.3 -langsmith==0.0.83 +langchain-core==0.1.18 +langchain-openai==0.0.5 +langsmith==0.0.86 markdown-it-py==3.0.0 markupsafe==2.1.4 mdurl==0.1.2 numpy==1.26.3 -openai==1.9.0 +openai==1.10.0 packaging==23.2 -pydantic==2.5.3 -pydantic-core==2.14.6 +pydantic==2.6.0 +pydantic-core==2.16.1 pydantic-settings==2.1.0 pygments==2.17.2 python-dotenv==1.0.1 @@ -44,4 +44,4 @@ tenacity==8.2.3 tiktoken==0.5.2 tqdm==4.66.1 typing-extensions==4.9.0 -urllib3==2.1.0 +urllib3==2.2.0 From 4c7d5345d8a1e294f191c785083a703684ac0776 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 2 Feb 2024 14:09:59 +0400 Subject: [PATCH 451/451] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20v0.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6fbb89b..7582a0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcchain" -version = "0.2.0-alpha.5" +version = "0.2.0" description = "🔖 write prompts as python functions" authors = [{ name = "Shroominic", email = "contact@shroominic.com" }] dependencies = [