From b4e9846728dffd6b71dd6e0bcfb743be73d558e2 Mon Sep 17 00:00:00 2001 From: AkshathRaghav Date: Wed, 15 May 2024 06:51:09 -0400 Subject: [PATCH] Major Changes for 1.0.0 Signed-off-by: AkshathRaghav --- .github/workflows/formatter.yml | 27 + README.md | 103 +- benchmarks/big_bench/benchmark.py | 40 +- benchmarks/hotpotqa/benchmark.py | 10 +- benchmarks/hotpotqa/out.txt | 4 +- benchmarks/llm_reasoning/benchmark.py | 10 +- formatter.sh | 8 + grammarflow/__init__.py | 1 + grammarflow/constrain.py | 149 ++- grammarflow/grammars/__init__.py | 2 +- grammarflow/grammars/error.py | 8 + grammarflow/grammars/gnbf.py | 62 +- grammarflow/grammars/json.py | 132 ++- grammarflow/grammars/template.py | 18 +- grammarflow/grammars/toml.py | 208 ++-- grammarflow/grammars/xml.py | 241 +++-- grammarflow/prompt/__init__.py | 2 +- grammarflow/prompt/builder.py | 148 ++- grammarflow/prompt/template.py | 191 ++-- grammarflow/tools/__init__.py | 2 +- grammarflow/tools/llm.py | 171 +-- grammarflow/tools/pydantic.py | 70 +- grammarflow/tools/response.py | 44 +- guide.ipynb | 1041 ++++++++++++++++++ pylintrc | 407 +++++++ samples/bert_finetuning/annotator.ipynb | 16 +- samples/demo.ipynb | 1304 ----------------------- 27 files changed, 2513 insertions(+), 1906 deletions(-) create mode 100644 .github/workflows/formatter.yml create mode 100644 formatter.sh create mode 100644 grammarflow/grammars/error.py create mode 100644 guide.ipynb create mode 100644 pylintrc delete mode 100644 samples/demo.ipynb diff --git a/.github/workflows/formatter.yml b/.github/workflows/formatter.yml new file mode 100644 index 0000000..9618121 --- /dev/null +++ b/.github/workflows/formatter.yml @@ -0,0 +1,27 @@ +name: Run formatting on the codebase. + +on: [push] + +jobs: + run-script: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + + - name: Install dependencies (if any) + run: | + python -m pip install --upgrade pip + pip install --upgrade autopep8 + + - name: Run the script + run: | + autopep8 --in-place --recursive . + + diff --git a/README.md b/README.md index d09a531..5677566 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,17 @@ ## 🤔 What is this? -This repository contains code to abstract the LLM output constraining process. It helps you define your grammar rules using Pydantic and Typing in a pythonic way, and inherently embeds metadata from these dataclasses into the prompt. Parsing is enabled in JSON, TOML and XML formats, with custom parsers that avoid the issues faced by `json.loads` (..etc) while parsing direct outputs. It can also create GNBF grammr from the same, which is used by the [llama.cpp](https://github.com/ggerganov/llama.cpp/) package for sampling logits smartly. +GrammarFlow abstracts the **LLM constraining process for complex-response tasks**. It helps you define your grammar rules using Pydantic and Typing in a pythonic way, and inherently embeds metadata from these dataclasses into the prompt. Parsing is enabled in JSON, TOML and XML formats, with custom parsers that avoid the issues faced by `json.loads` (..etc) while parsing direct outputs. -The goal of this package was to overcome the issues faced when using langchain's output parsers with instruct language models. While GPT-4 produces consistent results in returning the correct formats, Llama-7B would cause parsing errors in my testing chains with more complex prompts. +Importantly, the package supports the generation of **GNBF grammar**, which integrates seamlessly with the [llama.cpp](https://github.com/ggerganov/llama.cpp/) package. This integration allows for more intelligent sampling of logits, optimizing the response quality from models. -> Please reach out to `araviki [at] purdue [dot] edu` or open an issue on Github if you have any questions or inquiry related to GrammarFlow and its usage. +The goal of this package was to overcome the issues faced when using LangChain's output parsers with instruct language models locally. While GPT-4 produces consistent results in returning the correct formats, local models from families like Llama and Mistral would cause parsing errors in my testing chains when I need more than just a single string response. Recently, GrammarFlow was extended to cover more features to help anyone trying to work with LLMs for complex use-cases: multi-grammar generation, regex patterns, etc. + +Moreover, GrammarFlow is meant for use-cases with (any kind of) AI Agents, as well as extracting content from text or question-answering problems. This allows it to have an *edge over* batched LLM generation and schema recomposing. The above methods would require *much higher #calls* to an inference function, which will increase the total cost of an iteration if using a paid service like GPT or Gemini. + +Kindly go through [`Remarks!`](https://github.com/e-lab/SyntaxShaper/tree/main?tab=readme-ov-file#remarks) section to get a complete understanding of what we're doing. + +> Please reach out to `araviki[at]purdue[dot]edu` or open an issue on Github if you have any questions or inquiry related to GrammarFlow and its usage. ## Results: @@ -68,21 +74,37 @@ GrammarFlow was tested against popular LLM datasets, with a focus on constrainin |-------------------------------------------------------------------------------+------------------------| ``` -## ⚡ Quick Install + +## ⚡ Installation + +#### Quick Install `pip install grammarflow` +#### (Not so quick) Install + +``` +conda create --name grammarflow python=3.9 -y +conda activate grammarflow + +git clone https://github.com/e-lab/SyntaxShaper +cd grammarflow +pip install . +``` + ## 📃 Code Usage +> The [guide](https://github.com/e-lab/SyntaxShaper/blob/main/guide.ipynb) contains an in-depth explanation of all the classes and functions. + Map out what your agent chain is doing. Understand what it's goals are and what data needs to be carried forward from one step to the next. For example, consider the [ReAct prompting framework](https://react-lm.github.io/). In every call, we want to pass in the Action and subsequent Observation to the next call. ```python from grammarflow import * -from grammarflow.prompt.template import Agent -from grammarflow.grammars.template import AgentStep -from grammarflow.tools.LLM import LocalLlama +from grammarflow.prompt.template import Agent # Prompt +from grammarflow.grammars.template import AgentStep # Structured Grammar +from grammarflow.tools.llm import LocalLlama # Barebones inference call; interfaces with llama.cpp llm = LocalLlama() prompt = Agent() @@ -91,14 +113,15 @@ prompt = Agent() system_context = """Your goal is to think and plan out how to solve questions using agent tools provided to you. Think about all aspects of your thought process.""" user_message = """Who is Vladmir Putin?""" -with Constrain(prompt, 'xml') as manager: +with Constrain('xml') as manager: # Makes the changes to the prompt - manager.format_prompt( + prompt = manager.format( + prompt, placeholders={'prompt': user_message, 'instructions': system_context}, grammars=[{'model': AgentStep}] ) - llm_response = llm(manager.prompt, temperature=0.01) + llm_response = llm(prompt, temperature=0.01) # Parse the response into a custom dataclass for holding values response = manager.parse(llm_response) @@ -111,16 +134,17 @@ observation = PerformSomeAction( ## Features -GrammarFlow is mainly meant to be an add-on to your existing LLM applications. It works on the input to and output from your `llm()` call, treating everything in between as a black box. It contains pre-made template prompts for local GGUF models like [Llama2 (70B, 13B, 7B)](https://huggingface.co/TheBloke/Upstage-Llama-2-70B-instruct-v2-GGUF), [Mistral](https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF), [Mixtral](https://huggingface.co/TheBloke/Synthia-MoE-v3-Mixtral-8x7B-GGUF) and has template grammars for common tasks. Making these prompts and grammars are trivial and require minimal effort, as long as you know the format of what you're building. +GrammarFlow is mainly meant to be an add-on to your existing LLM applications. It works on the input to and output from your `llm()` call, treating everything in between as a black box. It contains pre-made template prompts for local GGUF models like [Llama2 (70B, 13B, 7B)](https://huggingface.co/TheBloke/Upstage-Llama-2-70B-instruct-v2-GGUF), [Mistral](https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF), [Mixtral](https://huggingface.co/TheBloke/Synthia-MoE-v3-Mixtral-8x7B-GGUF) and has template grammars for common tasks like Chain-of-Thought and Iterative Agents. Making these prompts and grammars are trivial and require minimal effort, as long as you know the format of what you're building. -- [X] **GBNF Support**: Converts any Pydantic model to GNBF grammar for using with [llama.cpp](https://github.com/ggerganov/llama.cpp/)'s token-based sampling. Enables adding regex patterns directly. +- [X] **GBNF Support**: Converts any Pydantic model to GNBF grammar for using with [llama.cpp](https://github.com/ggerganov/llama.cpp/)'s token-based sampling. Enables adding regex patterns directly through Pydantic's `Field(..., pattern="")`. - [x] **Easy Integration**: Integrates with any package or stack by just manipulating the prompt and decoding the result into a pythonic data abstractor. Treats everything in between as a **black box**. -- [x] **Handles Complex Grammars**: Can handle typing objects ('List', 'Dict', etc.) and nested Pydantic logic with complex data-types. -- [x] **Experiments with different 'formats'**: Defines grammar rules in XML, JSON and TOML formats. JSON is the standard, while XML is best for (+3) nested parsing and TOML is best when you want to get multiple models parsed simulatenously. Each has it's own usecase as described in the demo. -- [x] **Reduces hallucinations or garbage results during sampling**: GBNF grammars allow for controlled whitespacing/identation and model generation ordering, while parsing logic allows for ignoring incorrect terminal symbols. +- [x] **Handles Complex Grammars**: Can handle typing objects ('List', 'Dict', etc.) and nested Pydantic logic with complex data-types. +- [x] **Experiments with different 'formats'**: Defines grammar rules in XML, JSON and TOML formats. JSON is the standard, while XML is best for nested parsing and TOML is best when you want to get multiple models parsed simulatenously. Each has it's own usecase as described in the [guide](https://github.com/e-lab/SyntaxShaper/blob/main/guide.ipynb). +- [x] **Reduces hallucinations or garbage results during sampling**: GBNF grammars allow for controlled whitespacing/identation and model ordering, while parsing logic allows for ignoring incorrect terminal symbols. + ### Examples (@ samples/) -1. For a general overview of what GrammarFlow can do, look at [demo.ipynb](https://github.com/e-lab/SyntaxShaper/blob/main/samples/demo.ipynb). +1. For a general overview of what GrammarFlow can do, look at [guide.ipynb](https://github.com/e-lab/SyntaxShaper/blob/main/guide.ipynb). 2. For my modification to [ReAct's](https://github.com/ysymyth/ReAct) evaluation code on [HotPotQA](https://hotpotqa.github.io/), look at [hotpotqa_modified](https://github.com/e-lab/SyntaxShaper/blob/main/samples/hotpotqa/hotpotqa_modified.ipynb). 3. I've also added an implementation of a [data annotator](https://github.com/e-lab/SyntaxShaper/blob/main/samples/bert_finetuning/annotator.ipynb) for this [BERT fine-tuning guide](https://www.datasciencecentral.com/how-to-fine-tune-bert-transformer-with-spacy-3/). @@ -160,42 +184,59 @@ from grammarflow import GNBF grammar = GNBF(Project).generate_grammar() -# Verify with LlamaGrammar -GNBF.verify_grammar(grammar) +# Verify with LlamaGrammar from llama-cpp-python +GNBF.verify_grammar(grammar, format_='json') ``` Results: ``` -root ::= project ws -project ::= "{" ws "\"name\":" ws string "," ws "\"description\":" ws string "," ws "\"project-url\":" ws string "," ws "\"team-members\":" ws teammember "," ws "\"grammars\":" ws grammars "}" ws -ws ::= [ \t\n]* +root ::= ws Project +Project ::= nl "{" "\"Project\":" ws "{" ws "\"name\":" ws string "," nl "\"description\":" ws string "," nl "\"project-url\":" ws string "," nl "\"team-members\":" ws TeamMember "," nl "\"grammars\":" ws Task "}" ws "}" +ws ::= [ \t\n] +nl ::= [\n] string ::= "\"" ( [^"\\] | - "\\" (["\\/bfnrt] | "u" [0-9a-fa-f] [0-9a-fa-f] [0-9a-fa-f] [0-9a-fa-f]) + "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]) )* "\"" -teammember ::= "{" ws "\"name\":" ws string "," ws "\"role\":" ws string "}" ws -number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([ee] [-+]? [0-9]+)? -taskupdate ::= "{" ws "\"update-time\":" ws number "," ws "\"comment\":" ws string "," ws "\"status\":" ws status "}" ws +TeamMember ::= nl "{" ws "\"name\":" ws string "," nl "\"role\":" ws string "}" +number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? +boolean ::= ("True" | "False") +TaskUpdate ::= nl "{" ws "\"update-time\":" ws number "," nl "\"comment\":" ws string "," nl "\"status\":" ws boolean "}" array ::= "[" ws ( due-date-value ("," ws due-date-value)* )? "]" ws due-date-value ::= string -task ::= "{" ws "\"title\":" ws string "," ws "\"description\":" ws string "," ws "\"assigned-to\":" ws teammember "," ws "\"due-date\":" ws array "," ws "\"updates\":" ws taskupdate "}" ws +Task ::= nl "{" ws "\"title\":" ws string "," nl "\"description\":" ws string "," nl "\"assigned-to\":" ws TeamMember "," nl "\"due-date\":" ws array "," nl "\"updates\":" ws TaskUpdate "}" ``` -You can use this grammar to pass into [llama.cpp](https://github.com/ggerganov/llama.cpp/) through a barebones LLM class that is provided. +You can use this grammar to pass into [llama.cpp](https://github.com/ggerganov/llama.cpp/) through a [barebones LLM class](https://github.com/e-lab/SyntaxShaper/blob/main/grammarflow/tools/llm.py) that is provided. ```python +from grammarflow import LocalLlama + llm = LocalLlama() -response = llm(manager.prompt, grammar=manager.get_grammar(CoT), stop_at=manager.stop_at) + +with Constrain('xml') as manager: + prompt = manager.format(...) + response = llm(prompt, grammar=manager.get_grammar(CoT), stop_at=prompt.stop_at) ``` -## Remarks! +## Remarks + +Please keep in mind that this package is purely software driven and aims to make developers lives simpler. It can work across model families and parameter counts with great success in parsing. + +However, with an increase in complexity of the prompt, the accuracy and 'performance' of the model's thinking capability will degrade. This is attributed to the context-window problem that a lot of researchers are working to improve. LLMs are autoregressive models which track previously seen tokens in order to iteratively predict the next one, and thus provide (a lot) of token probabilities in every generation. Different decoding startegies like **nucleus sampling** (used in GPT) and **beam search** are expensive and need to be used in combination with other methods to prune bad thinking patterns at generation time. + +In language models, a larger prompt provides more context, leading to a wider range of plausible continuations and increasing the uncertainty in the next token's prediction. Mathematically, this manifests as a **higher entropy in the distribution** over possible next tokens, reflecting a greater number of likely sequences or "divergent trees" during decoding. Incorporating grammar-based constraining in language models forces the parsing of outputs to adhere to predefined syntactic rules, increasing the computational complexity and reducing flexibility in generation. This constriction **narrows the search space of possible outputs**, complicating the task of finding optimal sequences that satisfy both grammatical and contextual criteria. -Please keep in mind that this package is purely software driven and aims to make developers lives a little simpler. It can work across model families and parameter counts with great success in parsing. +This is why people have come up with great workarounds like prompting strategies, prompt pruning, batch processing prompts (like in [JSONFormer](https://github.com/1rgs/jsonformer/blob/main/jsonformer/) and [super-json-mode](https://github.com/varunshenoy/super-json-mode/blob/main/superjsonmode/)), etc. Using those practices along with this library **boosts the efficiency** of whatever you're building! -However, with an increase in complexity of the prompt, the accuracy and 'performance' of the model's thinking capability will fail. This is attributed to the greater possibility +> Batch-processing techniques entail generating simple strings in batches and subsequently formatting them into JSON structures manually. This approach, while straightforward, encounters significant limitations when the generated content requires internal consistency or interdependence among fields. + +For instance, take the generation of responses for a Chain of Thought (CoT) prompt. Traditional batch processing might yield a series of isolated responses, each reflecting distinct, possibly unrelated thought processes. When these responses need to be structured into a JSON format that adheres to a list, manual entry is not sufficient. This method lacks the capability to ensure that subsequent entries are contextually aligned with previous ones. + +This is where GrammarFlow steps in -- leveraging context-free grammars (CFGs) combined with carefully engineered prompts to guide the generation process. ## Citation diff --git a/benchmarks/big_bench/benchmark.py b/benchmarks/big_bench/benchmark.py index db3affc..556532c 100644 --- a/benchmarks/big_bench/benchmark.py +++ b/benchmarks/big_bench/benchmark.py @@ -30,11 +30,9 @@ def StrategyQA(model_name, get_prompt, llm, verbose=False, **kwargs): logs = {'n_badcalls': 0, 'n_calls': 0, 'n_goodcalls': 0, 'n_matchedcall': 0, 'responses': [], 'expected': []} for ind, example in enumerate(file_['examples'][:n]): - with Constrain(get_prompt(**kwargs)) as manager: - manager.set_config( - format='xml' - ) - manager.format_prompt( + with Constrain('json') as manager: + template = get_prompt(**kwargs) + prompt = manager.format( placeholders={ "instructions": file_['description'], "prompt": example['input'] @@ -48,7 +46,7 @@ def StrategyQA(model_name, get_prompt, llm, verbose=False, **kwargs): if verbose: print(manager.prompt) print('-------------------') - response = llm(manager.prompt, grammar=manager.get_grammar(StrategyQAModel), stop_at=manager.stop_at) + response = llm(prompt, grammar=manager.get_grammar(StrategyQAModel), stop_at=template.stop_at) if response: if verbose: print(response) @@ -83,11 +81,9 @@ def LogicGridPuzzle(model_name, get_prompt, llm, verbose=False, **kwargs): logs = {'n_badcalls': 0, 'n_calls': 0, 'n_goodcalls': 0, 'n_matchedcall': 0, 'responses': [], 'expected': []} for ind, example in enumerate(file_['examples'][:n]): - with Constrain(get_prompt(**kwargs)) as manager: - manager.set_config( - format='xml' - ) - manager.format_prompt( + with Constrain('xml') as manager: + template = get_prompt(**kwargs) + prompt = manager.format(template, placeholders={ "instructions": file_['description'], "prompt": example['input'] @@ -101,7 +97,7 @@ def LogicGridPuzzle(model_name, get_prompt, llm, verbose=False, **kwargs): if verbose: print(manager.prompt) print('-------------------') - response = llm(manager.prompt, grammar=manager.get_grammar(LogicGridPuzzleModel), stop_at=manager.stop_at) + response = llm(prompt, grammar=manager.get_grammar(LogicGridPuzzleModel), stop_at=template.stop_at) if response: if verbose: print(response) @@ -139,11 +135,9 @@ def PhysicsQuestions(model_name, get_prompt, llm, verbose=False, **kwargs): logs = {'n_badcalls': 0, 'n_calls': 0, 'n_goodcalls': 0, 'n_matchedcall': 0, 'responses': [], 'expected': []} for ind, example in enumerate(file_['examples'][:n]): - with Constrain(get_prompt(**kwargs)) as manager: - manager.set_config( - format='xml' - ) - manager.format_prompt( + with Constrain('json') as manager: + template = get_prompt(**kwargs) + prompt = manager.format(template, placeholders={ "instructions": file_['description'], "prompt": example['input'] @@ -156,7 +150,7 @@ def PhysicsQuestions(model_name, get_prompt, llm, verbose=False, **kwargs): if verbose: print('-------------------') - response = llm(manager.prompt, grammar=manager.get_grammar(PhysicsQuestionsModel), stop_at=manager.stop_at) + response = llm(prompt, grammar=manager.get_grammar(PhysicsQuestionsModel), stop_at=template.stop_at) if response: if verbose: print(response) @@ -193,11 +187,9 @@ def ReasoningAboutColors(model_name, get_prompt, llm, verbose=False, **kwargs): logs = {'n_badcalls': 0, 'n_calls': 0, 'n_goodcalls': 0, 'n_matchedcall': 0, 'responses': [], 'expected': []} for ind, example in enumerate(file_['examples'][:n]): - with Constrain(get_prompt(**kwargs)) as manager: - manager.set_config( - format='json' - ) - manager.format_prompt( + with Constrain('json') as manager: + template = get_prompt(**kwargs) + prompt = manager.format(template, placeholders={ "instructions": file_['description'], "prompt": example['input'] @@ -210,7 +202,7 @@ def ReasoningAboutColors(model_name, get_prompt, llm, verbose=False, **kwargs): if verbose: print('-------------------') - response = llm(manager.prompt, grammar=manager.get_grammar(Colors), stop_at=manager.stop_at) + response = llm(prompt, grammar=manager.get_grammar(Colors), stop_at=template.stop_at) if response: if verbose: print(response) diff --git a/benchmarks/hotpotqa/benchmark.py b/benchmarks/hotpotqa/benchmark.py index 79b32de..2eedc9f 100644 --- a/benchmarks/hotpotqa/benchmark.py +++ b/benchmarks/hotpotqa/benchmark.py @@ -125,11 +125,9 @@ def webthink(model_name, llm, idx=None, env=None, to_print=False): history_ = load_history(history) # Initializes history every run # The only major change we've made! - with Constrain(make_prompt(model_name)) as manager: - manager.set_config( - format='json' - ) - manager.format_prompt( + with Constrain('json') as manager: + template = make_prompt(model_name) + prompt = manager.format(template, placeholders={ "question": question, "history": history_, @@ -150,7 +148,7 @@ def webthink(model_name, llm, idx=None, env=None, to_print=False): print('Prompt too long. Breaking.') done = False break - response = llm(manager.prompt, grammar=manager.get_grammar(Step), stop_at=manager.stop_at) + response = llm(prompt, grammar=manager.get_grammar(Step), stop_at=template.stop_at) n_calls += 1 resp_ = response diff --git a/benchmarks/hotpotqa/out.txt b/benchmarks/hotpotqa/out.txt index c499d26..9d5e0bc 100644 --- a/benchmarks/hotpotqa/out.txt +++ b/benchmarks/hotpotqa/out.txt @@ -1,5 +1,5 @@ -************ +```````````` main: please use the 'perplexity' tool for perplexity calculations -************ +```````````` diff --git a/benchmarks/llm_reasoning/benchmark.py b/benchmarks/llm_reasoning/benchmark.py index 46ef0dd..307325b 100644 --- a/benchmarks/llm_reasoning/benchmark.py +++ b/benchmarks/llm_reasoning/benchmark.py @@ -52,11 +52,9 @@ def RandomTrue(model_name, get_prompt, llm, verbose=False, **kwargs): row = file_[example_][example__] - with Constrain(get_prompt(**kwargs)) as manager: - manager.set_config( - format='json' - ) - manager.format_prompt( + with Constrain('json') as manager: + template = get_prompt(**kwargs) + prompt = manager.format(template, placeholders={ "instructions": "You are a professor of various practices. You like to think through the steps you take in solving your goal. You will be presented with a 'goal'. Solve it by iteratively making observations.", "prompt": f"Facts: {row['question']}\n Goal: {row['query']}" @@ -70,7 +68,7 @@ def RandomTrue(model_name, get_prompt, llm, verbose=False, **kwargs): if verbose: print(manager.prompt) print('-------------------') - response = llm(manager.prompt, grammar=manager.get_grammar(CoT), stop_at=manager.stop_at) + response = llm(prompt, grammar=manager.get_grammar(CoT), stop_at=template.stop_at) if response: if verbose: print(response) diff --git a/formatter.sh b/formatter.sh new file mode 100644 index 0000000..6b1c571 --- /dev/null +++ b/formatter.sh @@ -0,0 +1,8 @@ +# Path: ./formatter.sh + +files=`ls ./grammarflow/*/*.py` +for eachfile in $files +do + autopep8 --in-place --aggressive --aggressive --max-line-length=80 --experimental $eachfile +done + diff --git a/grammarflow/__init__.py b/grammarflow/__init__.py index a6f8fcc..ca8cb45 100644 --- a/grammarflow/__init__.py +++ b/grammarflow/__init__.py @@ -1,4 +1,5 @@ from .constrain import Constrain from .prompt.builder import Prompt, PromptBuilder from .tools.response import Response +from .tools.llm import LocalLlama, OpenAI from .grammars.gnbf import GNBF \ No newline at end of file diff --git a/grammarflow/constrain.py b/grammarflow/constrain.py index 9be0677..59a9c86 100644 --- a/grammarflow/constrain.py +++ b/grammarflow/constrain.py @@ -2,85 +2,128 @@ from .grammars.toml import TOML from .grammars.xml import XML from .grammars.gnbf import GNBF +from .grammars.error import ParsingError, ConfigError # pylint: disable=unused-import from .prompt.builder import Prompt, PromptBuilder from .tools.response import Response import re +from typing import Union, Dict, List +from pydantic import BaseModel + class Constrain: - def __init__(self, prompt, format_='json', return_sequence='single_response'): + ''' + Context Manager initialized using `with` statement. + ''' + + + def __init__(self, format_: str ='json', return_sequence: str ='single_response'): + """ + Initializes the Constrain class. + + Args: + format_ (str): Serialization type. Default is 'json'. + return_sequence (str): Return sequence. Default is 'single_response'. + Returns: + Constrain context manager object. + """ + self.config = {} + + assert format_ in ['json', 'toml', 'xml'], "Serialization type must be one of 'json', 'toml', 'xml'." + assert return_sequence in ['single_response', 'multi_response'], "Return sequence must be one of 'single_response', 'multi_response'." + self.config["format"] = format_ self.config["return_sequence"] = return_sequence - # Keeps track of last run for inflation_rate() + + self.history = {} self.initial_prompt = None self.inflation = None self.stop_at = "" + self.idx = 1 + + def get_grammar(self, model: BaseModel) -> str: + ''' + Pydantic -> GNBF String + ''' + return GNBF(model).generate_grammar(self.config["format"]) + + def format(self, prompt: Union[str, PromptBuilder, Prompt], grammars: Union[Dict, List], placeholders: Dict = None, examples: Dict = None, enable_on: Dict = None): + ''' + Formats the prompt with the grammars provided. + ''' if isinstance(prompt, str): prompt_config = PromptBuilder() prompt_config.add_section(define_grammar=True) prompt_config.add_section(add_few_shot_examples=True) prompt_config.add_section(text=prompt) - self.prompt = prompt_config - elif isinstance(prompt, PromptBuilder) or isinstance(prompt, Prompt): - self.prompt = prompt - self.stop_at = prompt.stop_at - else: - raise ValueError("Prompt must be a string, a PromptBuilder or a Prompt object.") - - def get_grammar(self, model): - return GNBF(model).generate_grammar(self.config["format"]) + prompt = prompt_config + elif not (isinstance(prompt, PromptBuilder) or isinstance(prompt, Prompt)): + raise ConfigError("Prompt must be a string, a PromptBuilder or a Prompt object.") - def format_prompt(self, grammars, placeholders=None, examples=None, enable_on=None): - if not self.prompt: - raise ValueError("Prompt is not set!") if not grammars: - raise ValueError("You need to provide a list of grammars to format the prompt!") + raise ConfigError("You need to provide grammars to format the prompt!") - self.config["grammars"] = grammars - self.config["examples"] = examples - self.config["enable_on"] = enable_on + if isinstance(grammars, dict): + grammars = [grammars] + elif not isinstance(grammars, list): + raise ConfigError("`grammars` must be a dictionary or a list of dictionaries.") + + if examples: + if isinstance(examples, dict): + examples = [examples] + elif not isinstance(examples, list): + raise ConfigError("`examples` must be a dictionary or a list of dictionaries.") if not placeholders: placeholders = {} else: placeholders = {key: str(value) for key, value in placeholders.items()} - if isinstance(self.prompt, Prompt): - if not self.prompt.placeholders: - raise ValueError( - f"Since your prompt uses placeholders in the template, you need to provide `placeholders` too! Ensure they have these keys: {self.prompt.placeholders}." + config = {} + config["format"] = self.config["format"] + config["return_sequence"] = self.config["return_sequence"] + config["grammars"] = grammars + config["examples"] = examples + config["enable_on"] = enable_on + + self.history[self.idx] = {} + + if isinstance(prompt, Prompt): + if prompt.placeholders and not placeholders: + raise ConfigError("Since your prompt uses placeholders in the template, you need to provide `placeholders` parameter too! Ensure they have these keys: {prompt.placeholders} in this format - {'placeholder': 'value'}" ) - self.initial_prompt = self.prompt.prompt - elif isinstance(self.prompt, PromptBuilder): - self.initial_prompt = self.prompt.get_text() + self.history[self.idx]['initial_prompt'] = prompt.prompt + elif isinstance(prompt, PromptBuilder): if placeholders: - self.initial_prompt += " ".join(list([x for x in placeholders.values() if x])) + self.history[self.idx]['initial_prompt'] = prompt.get_text() + else: + self.history[self.idx]['initial_prompt'] = prompt.get_text() + prompt = prompt.build(config) - self.prompt = self.prompt.build(self.config) + for key, value in placeholders.items(): + if key in prompt.placeholders: + self.history[self.idx]['initial_prompt'] = self.history[self.idx]['initial_prompt'].replace(f"{{{key}}}", value) - self.prompt = self.prompt.fill(**placeholders) + prompt = prompt.fill(**placeholders) + + self.history[self.idx]['filled_prompt'] = prompt - def parse(self, return_value): + self.idx += 1 + + return prompt + + def parse(self, return_value: str): if not return_value: return None - if isinstance(return_value, str): return_value = return_value.replace('\\', '') # Removing escape; quite popular in local llms - try: - parsed_response = self.parse_helper(return_value) - except Exception as e: - print('Unable to parse: ', e) - return return_value - try: - return Response(parsed_response) # Custom class for getting values from response - except Exception as e: - print('Unable to make Response: ', e) - return parsed_response - - def parse_helper(self, return_value): + parsed_response = self.parse_helper(return_value) + return Response(parsed_response) # Custom class for getting values from response + + def parse_helper(self, return_value: Union[str, List]): # pylint: disable=missing-function-docstring if not self.config["format"]: - raise ValueError("Serialization type is not set!") + raise ConfigError("Serialization type is not set!") if isinstance(return_value, str): if '```' in return_value: @@ -112,23 +155,23 @@ def parse_helper(self, return_value): to_return += [self.parse([value])] return to_return - def inflation_rate(self): - if self.inflation: - return self.inflation + def inflation_rate(self, idx: int = -1): # pylint: disable=missing-function-docstring + if idx == -1: idx = self.idx - 1 - import tiktoken + import tiktoken # pylint: disable=import-outside-toplevel encoding = tiktoken.encoding_for_model("gpt-3.5-turbo") - self.inflation = { - "before": len(encoding.encode(self.initial_prompt)), - "after": len(encoding.encode(self.prompt)), + inflation = { + "before": len(encoding.encode(self.history[idx]['initial_prompt'])), + "after": len(encoding.encode(self.history[idx]['filled_prompt'])), } - self.inflation["factor"] = ( - f"{((self.inflation['after'] - self.inflation['before']) / self.inflation['before']):.1f}x" + + inflation["factor"] = ( + f"{((inflation['after'] - inflation['before']) / inflation['before']):.1f}x" ) - return self.inflation + return inflation def __enter__(self): return self diff --git a/grammarflow/grammars/__init__.py b/grammarflow/grammars/__init__.py index 54808c7..9690bcf 100644 --- a/grammarflow/grammars/__init__.py +++ b/grammarflow/grammars/__init__.py @@ -1,4 +1,4 @@ from .gnbf import GNBF from .json import JSON from .toml import TOML -from .xml import XML \ No newline at end of file +from .xml import XML diff --git a/grammarflow/grammars/error.py b/grammarflow/grammars/error.py new file mode 100644 index 0000000..ed05142 --- /dev/null +++ b/grammarflow/grammars/error.py @@ -0,0 +1,8 @@ +class ParsingError(Exception): + def __init__(self, message): + super().__init__(message) + + +class ConfigError(Exception): + def __init__(self, message): + super().__init__(message) diff --git a/grammarflow/grammars/gnbf.py b/grammarflow/grammars/gnbf.py index 6c754a7..7e32a06 100644 --- a/grammarflow/grammars/gnbf.py +++ b/grammarflow/grammars/gnbf.py @@ -1,11 +1,13 @@ -import json import re - -from llama_cpp import Llama, LlamaGrammar +from llama_cpp import LlamaGrammar from pydantic import BaseModel - +from typing import Dict class GNBF: + """ + Converts a Pydantic model to GNBF grammar. + """ + TYPE_RULES = { "boolean": r'("True" | "False")', "number": r'("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)?', @@ -35,25 +37,36 @@ def __init__(self, model: BaseModel): self.used_data_types = set() self.grammar_entries = [] - def add_rule(self, name, definition): + def add_rule(self, name: str, definition: str): sanitized_name = re.sub(r"[^a-zA-Z0-9-]+", "-", name) if sanitized_name not in self.rules or self.rules[sanitized_name] == definition: self.rules[sanitized_name] = definition return sanitized_name - def format_literal(self, literal, format_): + def format_literal(self, literal: str, format_: str): + new_literal = literal.replace( + '"', + '\\"').replace( + "\n", + "\\n").replace( + "\r", + "\\r") + if format_ == 'json': - return r'"\"' + literal.replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r") + r"\"" + return r'"\"' + new_literal + r"\"" else: - return literal.replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r") - return json.dumps(literal) + " ws" + return new_literal - def convert_type(self, json_type): + def convert_type(self, json_type: str): if json_type not in self.used_data_types: self.used_data_types.add(json_type) return json_type - def handle_schema(self, schema, format_, name="root",): + def handle_schema(self, schema: Dict, format_: str, name="root") -> str: + ''' + Recursively handles the schema and generates the grammar. + ''' + if name == "root" and "definitions" in schema: for definition_name, definition_schema in schema["definitions"].items(): self.handle_schema(definition_schema, format_, definition_name) @@ -68,7 +81,7 @@ def handle_schema(self, schema, format_, name="root",): if not schema_type: schema_type = schema.get("type") else: - return schema_type + return f'"{schema_type}"' if not schema_type: if "anyOf" in schema: @@ -83,6 +96,11 @@ def handle_schema(self, schema, format_, name="root",): for prop, prop_schema in properties.items(): prop_name = self.format_literal(prop, format_) prop_type = self.handle_schema(prop_schema, format_, prop) + + if prop_schema.get('pattern', None): + self.grammar_entries.insert(0, f'{prop} ::= {prop_schema["pattern"]}') + prop_type = f"{prop}" + if format_ == 'json': prop_definitions.append(f'{prop_name}:" ws {prop_type}') elif format_ == 'xml': @@ -90,27 +108,30 @@ def handle_schema(self, schema, format_, name="root",): elif format_ == 'toml': if prop_type.strip()[0].islower(): prop_definitions.append(f'"{prop_name}" ws "=" ws {prop_type}') else: prop_definitions.append(f'{prop_type}') + if format_ == 'json': properties_rule = r' "," nl '.join(prop_definitions) if name == 'root': rule = f'nl "{{" {self.format_literal(self.json_obj['title'], 'json')}:" ws "{{" ws {properties_rule} "}}" ws "}}"' else: rule = f'nl "{{" ws {properties_rule} "}}"' elif format_ == 'xml': properties_rule = r' ws '.join(prop_definitions) - if name == 'root': rule = f'"<{self.json_obj['title']}>" ws {properties_rule} ws ""' + if name == 'root': rule = f'"<{self.format_literal(self.json_obj['title'], 'xml')}>" ws {properties_rule} ws ""' else: rule = f'{properties_rule}' elif format_ == 'toml': properties_rule = r' nl '.join(prop_definitions) - rule = f'"[{self.json_obj['title']}]" nl {properties_rule}' - if name == 'root': rule = f'"[{self.json_obj['title']}]" nl {properties_rule}' + if name == 'root': rule = f'"[{self.format_literal(self.json_obj['title'], 'toml')}]" nl {properties_rule}' else: rule = f'{properties_rule}' + if name == "root": self.grammar_entries.insert(0, f"{self.json_obj['title']} ::= {rule}") self.grammar_entries.insert(0, f"{name} ::= ws {self.json_obj['title']}") else: self.add_rule(name, rule) + elif schema_type in self.TYPE_RULES: self.add_rule(schema_type, self.TYPE_RULES[schema_type]) return self.convert_type(schema_type) + elif schema_type in self.OBJECT_RULES: rule, types = self.OBJECT_RULES[schema_type] rule = rule.format(value=f"{name}-value").strip() @@ -129,19 +150,22 @@ def handle_schema(self, schema, format_, name="root",): self.convert_type(t) return self.add_rule(schema_type, rule) + elif "enum" in schema: enum_values = [self.format_literal(v) for v in schema["enum"]] return self.add_rule(name, " | ".join(enum_values)) + elif "const" in schema: return self.format_literal(schema["const"]) return name - def generate_grammar(self, format_='json'): + def generate_grammar(self, format_: str ='json') -> str: self.handle_schema(self.json_obj, format_) - self.grammar_entries += [f"{name} ::= {rule}" for name, rule in self.rules.items()] + self.grammar_entries += [f"{name} ::= {rule}" for name, + rule in self.rules.items()] return "\n".join(self.grammar_entries).replace("_", "-") @staticmethod - def verify_grammar(grammar: str): - return LlamaGrammar.from_string(grammar) \ No newline at end of file + def verify_grammar(grammar: str) -> str: + return LlamaGrammar.from_string(grammar) diff --git a/grammarflow/grammars/json.py b/grammarflow/grammars/json.py index c67f837..fbe482c 100644 --- a/grammarflow/grammars/json.py +++ b/grammarflow/grammars/json.py @@ -1,36 +1,51 @@ -from typing import List, Optional +from grammarflow.tools.pydantic import ModelParser +from grammarflow.grammars.error import ParsingError +from typing import List, Dict from pydantic import BaseModel -from grammarflow.tools.pydantic import ModelParser - class JSON: + """ + Handles JSON format generation from pydantic and parsing of XML strings. + """ + @staticmethod def format(model: BaseModel): + """ + Single model JSON format generation. + """ + grammar = "" - - fields, is_nested_model = ModelParser.extract_fields_with_descriptions([model]) + + fields, is_nested_model = ModelParser.extract_fields_with_descriptions([ + model]) if is_nested_model: - format_ = JSON.generate_prompt_from_fields({name: fields[name]}) - del fields[name] + format_ = JSON.generate_prompt_from_fields( + {model.__name__: fields[model.__name__]}) + del fields[model.__name__] else: format_ = JSON.generate_prompt_from_fields(fields) - grammar += f"***\n{format_}\n***\n" + grammar += f"```\n{format_}\n```\n" if is_nested_model: - grammar += "Use the data types given below to fill in the above model\n***\n" - for nested_model in fields: - grammar += f"{JSON.generate_prompt_from_fields({nested_model: fields[nested_model]})}\n" - grammar += "***" - - return grammar + grammar += "Use the data types given below to fill in the above model\n```\n" + for nested_model_name, nested_schema in fields.items(): + grammar += f"{JSON.generate_prompt_from_fields( + {nested_model_name: nested_schema})}\n" + grammar += "```" + + return grammar @staticmethod def make_format(grammars: List[dict], return_sequence: str) -> str: - grammar, model_names, model_descrip, name = "", [], None, None + """ + Multiple model JSON format generation. Specific for use in .prompt.builder.PromptBuilder. + """ + + grammar, model_names, model_descrip, name = "", [], None, None for task in grammars: model = task.get("model") @@ -50,51 +65,71 @@ def make_format(grammars: List[dict], return_sequence: str) -> str: if name: model_names.append(f'"{name}"') - fields, is_nested_model = ModelParser.extract_fields_with_descriptions(model) + fields, is_nested_model = ModelParser.extract_fields_with_descriptions( + model) if is_nested_model: - format_ = JSON.generate_prompt_from_fields({name: fields[name]}) + format_ = JSON.generate_prompt_from_fields( + {name: fields[name]}) del fields[name] else: if len(fields) > 1: - format_ = JSON.generate_prompt_from_fields(fields, nested=True) + format_ = JSON.generate_prompt_from_fields( + fields, nested=True) else: format_ = JSON.generate_prompt_from_fields(fields) if model_descrip: grammar += f"{model_descrip}:\n" - else: - grammar += f"{name}:\n" - grammar += f"***\n{format_}\n***\n" + grammar += f"```\n{format_}\n```\n" if is_nested_model: - grammar += "Use the data types given below to fill in the above model\n***\n" - for nested_model in fields: - grammar += ( - f"{JSON.generate_prompt_from_fields({nested_model: fields[nested_model]}, ignore=True)}\n" - ) - grammar += "***" + grammar += "Use the data types given below to fill in the above model\n```\n" + for nested_model_name, nested_schema in fields.items(): + grammar += f"{JSON.generate_prompt_from_fields( + {nested_model_name: nested_schema})}\n" + grammar += "```" return grammar, model_names @staticmethod - def generate_prompt_from_fields(fields_info: dict, nested: bool = False, ignore=False) -> str: + def generate_prompt_from_fields( + fields_info: dict, + nested: bool = False, + ignore: bool = False) -> str: + """ + Takes in formatted schema from .tools.pydantic.ModelParser and generates a representation for the prompt. + """ + if not ignore: prompt_lines = ["{"] else: prompt_lines = [] + for model_name, fields in fields_info.items(): - model_prompt = JSON._generate_single_model_prompt(fields, model_name, nested=True) + model_prompt = JSON._generate_single_model_prompt( + fields, model_name, nested=True) prompt_lines.append(f"{model_prompt},") + prompt_lines[-1] = prompt_lines[-1].rstrip(",") + if not ignore: prompt_lines.append("}") + return "\n".join(prompt_lines) @staticmethod - def _generate_single_model_prompt(fields: dict, model_name: str, nested: bool = False) -> str: + def _generate_single_model_prompt( + fields: dict, + model_name: str, + nested: bool = False) -> str: + ''' + Helper function to generate prompt for a single model. + ''' + prompt_lines = [f'"{model_name}": ' + "{" if nested else "{"] + for var_name, details in fields.items(): line = f'"{var_name}": ' if "value" in details: @@ -107,16 +142,23 @@ def _generate_single_model_prompt(fields: dict, model_name: str, nested: bool = pass # Expected to be required else: line += " | Optional" - if str(details.get("default")) not in ["PydanticUndefined", "None"]: + if str(details.get("default")) not in [ + "PydanticUndefined", "None"]: line += f' | Default: "{details["default"]}"' line += "," prompt_lines.append(line) + prompt_lines[-1] = prompt_lines[-1].rstrip(",") prompt_lines.append(" }" if nested else "}") + return "\n".join(prompt_lines) @staticmethod - def parse_json(json_string): + def parse_json(json_string) -> Dict: + """ + JSON character-level parsing. + """ + def parse_value(json_string, i): if json_string[i] == "{": return parse_object(json_string, i + 1) @@ -126,11 +168,11 @@ def parse_value(json_string, i): return parse_number(json_string, i) elif json_string[i] == '"': return parse_string(json_string, i + 1) - elif json_string[i : i + 4].lower() == "true": + elif json_string[i: i + 4].lower() == "true": return True, i + 4 - elif json_string[i : i + 5].lower() == "false": + elif json_string[i: i + 5].lower() == "false": return False, i + 5 - elif json_string[i : i + 4].lower() in ["null", 'none']: + elif json_string[i: i + 4].lower() in ["null", 'none']: return None, i + 4 else: raise ValueError(f"Invalid character at {i}: {json_string[i]}") @@ -139,7 +181,7 @@ def parse_string(json_string, i): start = i while json_string[i] != '"': i += 1 - if json_string[start:i]: + if json_string[start:i]: return json_string[start:i], i + 1 return None, i + 1 @@ -168,7 +210,9 @@ def parse_object(json_string, i): key = key.replace("-", "_") i = skip_whitespace(json_string, i) if json_string[i] != ":": - raise ValueError(f'Expected ":" at {i}, got {json_string[i]}') + raise ValueError( + f'Expected ":" at {i}, got { + json_string[i]}') i = skip_whitespace(json_string, i + 1) value, i = parse_value(json_string, i) obj[key] = value @@ -182,7 +226,19 @@ def skip_whitespace(json_string, i): i += 1 return i - return parse_value(json_string, skip_whitespace(json_string, 0))[0] + def prune_starting(json_string): + index = json_string.find('{') + if index == 0 or index == -1: + return json_string + else: + return json_string[index:] + + try: + json_string = prune_starting(json_string) + return parse_value(json_string, skip_whitespace(json_string, 0))[0] + except BaseException as exc: + raise ParsingError( + 'ERROR: Unable to parse response into JSON format!') from exc @staticmethod def parse(text): diff --git a/grammarflow/grammars/template.py b/grammarflow/grammars/template.py index 62f1d5e..3b6f24f 100644 --- a/grammarflow/grammars/template.py +++ b/grammarflow/grammars/template.py @@ -1,12 +1,16 @@ from pydantic import BaseModel, Field -from typing import List +from typing import List class AgentStep(BaseModel): - thought: str = Field(..., description="Concisely describe your thought process in your current thinking step. Use your previous step's output to guide your current step.") - action: str = Field(..., description="Only return the tool.") - action_input: str = Field(..., description="Your input to the above action.") + thought: str = Field(..., description="Concisely describe your thought process in your current thinking step. Use your previous step's output to guide your current step.") + action: str = Field(..., description="Only return the tool.") + action_input: str = Field(..., + description="Your input to the above action.") -class CoT(BaseModel): - chain_of_thought: List[str] = Field(..., description="Iteratively list out the observations you make in solving your goal. Think out loud.") - answer: bool = Field(..., description="Options: True OR False.") \ No newline at end of file + +class CoT(BaseModel): + chain_of_thought: List[str] = Field( + ..., + description="Iteratively list out the observations you make in solving your goal. Think out loud.") + answer: bool = Field(..., description="Options: True OR False.") diff --git a/grammarflow/grammars/toml.py b/grammarflow/grammars/toml.py index e8135a0..bf0b3d9 100644 --- a/grammarflow/grammars/toml.py +++ b/grammarflow/grammars/toml.py @@ -1,124 +1,151 @@ -from typing import List, Optional +from grammarflow.tools.pydantic import ModelParser +from grammarflow.grammars.error import ParsingError +from typing import List, Dict from pydantic import BaseModel -from grammarflow.tools.pydantic import ModelParser - class TOML: + ''' + Handles TOML format generation from pydantic and parsing of XML strings. + ''' + @staticmethod def format(model: BaseModel): - grammar = "" - - fields, is_nested_model = ModelParser.extract_fields_with_descriptions([model]) + ''' + Single model tOML format generation. + ''' + + grammar = '' + + fields, is_nested_model = ModelParser.extract_fields_with_descriptions([ + model]) if is_nested_model: - format_ = TOML.generate_prompt_from_fields({name: fields[name]}) - del fields[name] + format_ = TOML.generate_prompt_from_fields( + {model.__name__: fields[model.__name__]}) + del fields[model.__name__] else: format_ = TOML.generate_prompt_from_fields(fields) - grammar += f"***\n{format_}\n***\n" + grammar += f'```\n{format_}\n```\n' if is_nested_model: - grammar += "Use the data types given below to fill in the above model\n***\n" - for nested_model in fields: - grammar += f"{TOML.generate_prompt_from_fields({nested_model: fields[nested_model]})}\n" - grammar += "***\n" - - return grammar + grammar += 'Use the data types given below to fill in the above model\n```\n' + for nested_model_name, nested_schema in fields.items(): + grammar += f"{TOML.generate_prompt_from_fields( + {nested_model_name: nested_schema})}\n" + grammar += '```\n' + + return grammar @staticmethod def make_format(grammars: List[dict], return_sequence: str) -> str: - grammar, model_names, model_descrip, name = "", [], None, None + ''' + Multiple model TOML format generation. Specific for use in .prompt.builder.PromptBuilder. + ''' + + grammar, model_names, model_descrip, name = '', [], None, None for task in grammars: - model = task.get("model") + model = task.get('model') if isinstance(model, list): - if not task.get("query"): - name = "_".join([m.__name__ for m in model]) + if not task.get('query'): + name = '_'.join([m.__name__ for m in model]) else: - if task.get("description"): - model_descrip = task.get("description") - if hasattr(model, "__name__"): + if task.get('description'): + model_descrip = task.get('description') + if hasattr(model, '__name__'): name = model.__name__ model = [model] - if task.get("query"): - name = "For query: " + repr(task.get("query")) + if task.get('query'): + name = 'For query: ' + repr(task.get('query')) if name: model_names.append(f'[{name}]') - fields, is_nested_model = ModelParser.extract_fields_with_descriptions(model) + fields, is_nested_model = ModelParser.extract_fields_with_descriptions( + model) if is_nested_model: - format_ = TOML.generate_prompt_from_fields({name: fields[name]}) + format_ = TOML.generate_prompt_from_fields( + {name: fields[name]}) del fields[name] else: format_ = TOML.generate_prompt_from_fields(fields) if model_descrip: - grammar += f"{model_descrip}:\n" - else: - grammar += f"{name}:\n" + grammar += f'{model_descrip}:\n' - grammar += f"***\n{format_}\n***\n" + grammar += f'```\n{format_}\n```\n' if is_nested_model: - grammar += "Use the data types given below to fill in the above model\n***\n" - for nested_model in fields: - grammar += f"{TOML.generate_prompt_from_fields({nested_model: fields[nested_model]})}\n" - grammar += "***\n" + grammar += 'Use the data types given below to fill in the above model\n```\n' + for nested_model_name, nested_schema in fields.items(): + grammar += f"{TOML.generate_prompt_from_fields( + {nested_model_name: nested_schema})}\n" + grammar += '```\n' return grammar, model_names @staticmethod def generate_prompt_from_fields(fields_info: dict) -> str: + ''' + Takes in formatted schema from .tools.pydantic.ModelParser and generates a representation for the prompt. + ''' + prompt_lines = [] for model_name, fields in fields_info.items(): - prompt_lines.append(f"[{model_name}]") + prompt_lines.append(f'[{model_name}]') for var_name, details in fields.items(): - line = f"{var_name} = " - if "value" in details: - line += f'"{details["value"]}"' + line = f'{var_name} = ' + if 'value' in details: + line += f'"{details['value']}"' else: - if details.get("type") == "boolean": - line += f'# Type: "{details["type"]}"' + if details.get('type') == 'boolean': + line += f'# Type: "{details['type']}"' else: - line += f'# Type: {details["type"]}' - if details.get("description"): - line += f' | "{details["description"]}"' + line += f'# Type: {details['type']}' + if details.get('description'): + line += f' | "{details['description']}"' - if str(details.get("default")) not in ["PydanticUndefined", "None"]: - line += f', Default: "{details["default"]}"' + if str(details.get('default')) not in [ + 'PydanticUndefined', 'None']: + line += f', Default: "{details['default']}"' prompt_lines.append(line) - return "\n".join(prompt_lines) + return '\n'.join(prompt_lines) @staticmethod - def parse_toml(toml_string): + def parse_toml(toml_string: str) -> Dict: + ''' + TOML character-level parsing. + ''' + def parse_section(toml_string, i): start = i - while toml_string[i] != "]": + while toml_string[i] != ']': i += 1 - key = toml_string[start:i].strip().replace("-", "_") + key = toml_string[start:i].strip().replace('-', '_') i = skip_whitespace(toml_string, i + 1) section = {key: {}} - while i < len(toml_string) and toml_string[i] not in "[": + while i < len(toml_string) and toml_string[i] not in '[': subkey, i = parse_key(toml_string, i) i = skip_whitespace(toml_string, i) - if toml_string[i] == "=": + if toml_string[i] == '=': i = skip_whitespace(toml_string, i + 1) value, i = parse_value(toml_string, i) - section[key.replace("\n", "")][subkey.replace("\n", "").strip()] = value + section[key.replace('\n', '')][subkey.replace( + '\n', '').strip()] = value i = skip_whitespace(toml_string, i) return section, i def parse_value(toml_string, i): + i = skip_whitespace(toml_string, i) if toml_string[i] == '"': return parse_string(toml_string, i + 1) - elif toml_string[i] == "[": + elif toml_string[i] == '[': return parse_array(toml_string, i) - elif toml_string[i] == "{": + elif toml_string[i] == '{': return parse_dict(toml_string, i) else: return parse_other(toml_string, i) @@ -131,10 +158,10 @@ def parse_string(toml_string, i): def parse_other(toml_string, i): start = i - if toml_string[start : start + 4].lower() == "true": - return True - elif toml_string[start : start + 5].lower() == "false": - return False + if toml_string[start:start+4].lower() == 'true': + return True, i + 4 + elif toml_string[start:start+5].lower() == 'false': + return False, i + 5 while toml_string[i] in '01234567890.': i += 1 @@ -147,56 +174,75 @@ def parse_other(toml_string, i): def parse_array(toml_string, i): i = skip_whitespace(toml_string, i + 1) - start = i - while toml_string[i] != "]": - i += 1 - return eval(toml_string[start:i]), i + 1 + array = [] + while toml_string[i] != ']': + value, i = parse_value(toml_string, i) + array += [value] + if toml_string[i] == ',': + i += 1 + i = skip_whitespace(toml_string, i) + return array, i + 1 def parse_key(toml_string, i): start = i - while toml_string[i] not in "=": + while toml_string[i] not in '=': i += 1 return toml_string[start:i], i def parse_dict(toml_string, i): dictionary = {} - i = skip_whitespace(toml_string, i + 1) # Move past the '{' - while toml_string[i] != "}": + i = skip_whitespace(toml_string, i + 1) # Move past the "{' + while toml_string[i] != '}': key, i = parse_key(toml_string, i) i = skip_whitespace(toml_string, i) - if toml_string[i] == "=" or toml_string[i] == ":": + if toml_string[i] == '=' or toml_string[i] == ':': i += 1 # Skip the '=' i = skip_whitespace(toml_string, i) value, i = parse_value(toml_string, i) dictionary[key] = value i = skip_whitespace(toml_string, i) - if toml_string[i] == ",": + if toml_string[i] == ',': i += 1 # Skip the comma i = skip_whitespace(toml_string, i) return dictionary, i + 1 def skip_whitespace(toml_string, i): - while i < len(toml_string) and toml_string[i] in [" ", "\n", "\t", "\r"]: + while i < len(toml_string) and toml_string[i] in [ + ' ', '\n', '\t', '\r']: i += 1 return i + def prune_starting(toml_string): + index = toml_string.find('[') + if index == 0 or index == -1: + return toml_string + else: + return toml_string[index:] + i = 0 storage = {} - while i < len(toml_string): - i = skip_whitespace(toml_string, i) - if i < len(toml_string) and toml_string[i] == "[": - i = skip_whitespace(toml_string, i + 1) - section, i = parse_section(toml_string, i) - key = list(section.keys())[0] - if key in storage: - storage[key.replace(' ', '')].append(section[key]) + + toml_string = prune_starting(toml_string) + + try: + while i < len(toml_string): + i = skip_whitespace(toml_string, i) + if i < len(toml_string) and toml_string[i] == '[': + i = skip_whitespace(toml_string, i + 1) + section, i = parse_section(toml_string, i) + key = list(section.keys())[0] + if key in storage: + storage[key.replace(' ', '')].append(section[key]) + else: + storage[key.replace(' ', '')] = [section[key]] else: - storage[key.replace(' ', '')] = [section[key]] - else: - break + break - return storage + return storage + except BaseException as exc: + raise ParsingError( + 'ERROR: Unable to parse response into TOML format!') from exc @staticmethod - def parse(text): + def parse(text: str): return TOML.parse_toml(text) diff --git a/grammarflow/grammars/xml.py b/grammarflow/grammars/xml.py index cd001e4..4289d02 100644 --- a/grammarflow/grammars/xml.py +++ b/grammarflow/grammars/xml.py @@ -1,60 +1,76 @@ -from collections import deque -from typing import List, Optional +from grammarflow.tools.pydantic import ModelParser +from grammarflow.grammars.error import ParsingError import re -from pydantic import BaseModel -from grammarflow.tools.pydantic import ModelParser +from collections import deque +from typing import List, Dict +from pydantic import BaseModel class XML: + ''' + Handles XML format generation from pydantic and parsing of XML strings. + ''' + @staticmethod def format(model: BaseModel): - grammar = "" - - fields, is_nested_model = ModelParser.extract_fields_with_descriptions([model]) + ''' + Single model XML format generation. + ''' + + grammar = '' + + fields, is_nested_model = ModelParser.extract_fields_with_descriptions([ + model]) if is_nested_model: - format_ = XML.generate_prompt_from_fields({name: fields[name]}) - del fields[name] + format_ = XML.generate_prompt_from_fields( + {model.__name__: fields[model.__name__]}) + del fields[model.__name__] else: format_ = XML.generate_prompt_from_fields(fields) - grammar += f"***\n{format_}\n***\n" + grammar += f"```\n{format_}\n```\n" if is_nested_model: - grammar += "Use the data types given below to fill in the above model\n***\n" - for nested_model in fields: - grammar += f"{XML.generate_prompt_from_fields({nested_model: fields[nested_model]})}\n" - grammar += "***\n" - - return grammar + grammar += 'Use the data types given below to fill in the above model\n```\n' + for nested_model_name, nested_schema in fields.items(): + grammar += f"{XML.generate_prompt_from_fields( + {nested_model_name: nested_schema})}\n" + grammar += '```\n' + + return grammar @staticmethod def make_format(grammars: List[dict], return_sequence: str) -> str: - grammar, model_names, model_descrip, name = "", [], None, None + ''' + Multiple model XML format generation. Specific for use in .prompt.builder.PromptBuilder. + ''' + grammar, model_names, model_description, name = '', [], None, None for task in grammars: - model = task.get("model") + model = task.get('model') if isinstance(model, list): - if not task.get("query"): - name = "_".join([m.__name__ for m in model]) + if not task.get('query'): + name = '_'.join([m.__name__ for m in model]) else: - if task.get("description"): - model_descrip = task.get("description") - if hasattr(model, "__name__"): + if task.get('description'): + model_description = task.get('description') + if hasattr(model, '__name__'): name = model.__name__ model = [model] - if task.get("query"): - name = "For query: " + repr(task.get("query")) + if task.get('query'): + name = 'For query: ' + repr(task.get('query')) if name: model_names.append(f'<{name}>') - fields, is_nested_model = ModelParser.extract_fields_with_descriptions(model) + fields, is_nested_model = ModelParser.extract_fields_with_descriptions( + model) if is_nested_model: format_ = XML.generate_prompt_from_fields({name: fields[name]}) @@ -62,42 +78,51 @@ def make_format(grammars: List[dict], return_sequence: str) -> str: else: format_ = XML.generate_prompt_from_fields(fields) - if model_descrip: - grammar += f"{model_descrip}:\n" - else: - grammar += f"{name}:\n" + if model_description: + grammar += f"{model_description}:\n" - grammar += f"***\n{format_}\n***\n" + grammar += f"```\n{format_}\n```\n" if is_nested_model: - grammar += "Use the data types given below to fill in the above model\n***\n" - for nested_model in fields: - grammar += f"{XML.generate_prompt_from_fields({nested_model: fields[nested_model]})}\n" - grammar += "***\n" + grammar += 'Use the data types given below to fill in the above model\n```\n' + for nested_model_name, nested_schema in fields.items(): + grammar += f"{XML.generate_prompt_from_fields( + {nested_model_name: nested_schema})}\n" + grammar += '```\n' return grammar, model_names @staticmethod - def generate_prompt_from_fields(fields_info: dict) -> str: + def generate_prompt_from_fields(fields_info: Dict) -> str: + ''' + Takes in formatted schema from .tools.pydantic.ModelParser and generates a representation for the prompt. + ''' + prompt_lines = [] for model_name, fields in fields_info.items(): - prompt_lines.append(f"<{model_name}>") + prompt_lines.append(f'<{model_name}>') for var_name, details in fields.items(): line = f"<{var_name}>" - if "value" in details: - line += f' {details["value"]} ' + if 'value' in details: + line += f' {details['value']} ' else: - line += f' #{details["type"]}# ' - if details.get("description"): - line += f' # {details["description"]}' - if str(details.get("default")) not in ["PydanticUndefined", "None"]: - line += f' # Default: "{details["default"]}"' + line += f' {details["type"]} ' + if details.get('description'): + line += f' # {details['description']}' + if str(details.get('default')) not in [ + 'PydanticUndefined', 'None']: + line += f' # Default: "{details['default']}"' prompt_lines.append(line) - prompt_lines.append(f"\n") + prompt_lines.append(f'\n') - return "\n".join(prompt_lines) + return '\n'.join(prompt_lines) - def parse(xml_string): + @staticmethod + def parse_xml(xml_string: str) -> Dict: + ''' + XML character-level parsing. + Extracts tags and values, considers complex nesting, builds skeleton structure and populates it. + ''' def build_structure(tags): result = {} @@ -123,7 +148,7 @@ def build_structure(tags): else: stack.pop() - result[tag] = objects[tags[0]] + result[tags[-1]] = objects[tags[0]] for tag, value in result.items(): if isinstance(value, list): @@ -142,14 +167,14 @@ def populate_structure(structure, storage): for tag, value in current.items(): if isinstance(value, dict) and not value: if tag in storage and storage[tag]: - current[tag] = storage[tag].pop(0)["value"] + current[tag] = storage[tag].pop(0)['value'] continue elif isinstance(value, list): for item in value: queue.append((item, tag)) else: queue.append((value, tag)) - elif isinstance(current, list) and parent_tag not in ["grammars", "grammars"]: + elif isinstance(current, list) and parent_tag not in ['grammars', 'grammars']: for item in current: if isinstance(item, dict): queue.append((item, parent_tag)) @@ -161,34 +186,41 @@ def parse_tags(tags, storage): def find_attr_value_pairs(tag): pattern = r'\b(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\')' - + matches = re.findall(pattern, tag) results = [] for attr, value_double, value_single in matches: value = value_double if value_double else value_single results.append((attr, value)) - + return results def parse_tag(xml_string, i): start = i - while xml_string[i] != ">": + while xml_string[i] != '>': i += 1 tag = xml_string[start:i] attr_value_pairs = find_attr_value_pairs(tag) - if attr_value_pairs: + if attr_value_pairs: for attr, value in attr_value_pairs: - tag = tag.replace('=', '').replace(attr, '').replace(value, '') + tag = tag.replace( + '=', + '').replace( + attr, + '').replace( + value, + '') - return tag.replace(' ', '').replace('/', '').replace('"', '').replace("-", "_"), i + 1, attr_value_pairs + return tag.replace(' ', '').replace( + '/', '').replace('"', '').replace('-', '_'), i + 1, attr_value_pairs def add_val(tag, value): value = evaluate(value) temp = {} - if not (value == None): - if isinstance(value, str) and value.strip() in ["\n", "", " "]: + if value: + if isinstance(value, str) and value.strip() in ['\n', '', ' ']: pass else: temp.update({"value": value}) @@ -200,76 +232,89 @@ def add_val(tag, value): else: storage[tag] = [temp] - def parse_value(xml_string, i): + def parse_value(xml_string: str, i: int) -> Dict: i = skip_whitespace(xml_string, i) start = i - temp_xml = xml_string.replace(" < ", "lsr") # In case, '<' is used in the value + # In case, '<' is used in the value + temp_xml = xml_string.replace(' < ', 'lsr') + # In case, '<' is used in the value + temp_xml = temp_xml.replace(' <= ', 'lsre') if temp_xml[i] == '"': i += 1 start = i while temp_xml[i] != '"': i += 1 - else: - while temp_xml[i] != "<": + else: + while temp_xml[i] != '<': i += 1 value = temp_xml[start:i].strip() - if value == "": + if value == '': return None, i else: - return value.replace("lsr", " < "), i + return value.replace('lsre', ' <= ').replace('lsr', ' < '), i def evaluate(value): try: - if value.lower().replace('"', '') == "true": + if value.lower().replace('"', '') == 'true': return True - elif value.lower().replace('"', '') == "false": + elif value.lower().replace('"', '') == 'false': return False - elif value.lower().replace('"', '') in ["null", "none"]: + elif value.lower().replace('"', '') in ['null', 'none']: return None - else: + else: return eval(value) - except: + except BaseException: return value - - def check_list(value): - try: - return list(value) - except: - return False def skip_whitespace(xml_string, i): - while i < len(xml_string) and xml_string[i] in " \t\r": + while i < len(xml_string) and xml_string[i] in ' \t\r': i += 1 return i + def prune_starting(xml_string): + index = xml_string.find('<') + if index == 0 or index == -1: + return xml_string + else: + return xml_string[index:] + i = 0 storage = {} tags = [] - - while i < len(xml_string): - i = skip_whitespace(xml_string, i) - if i >= len(xml_string): - break - if xml_string[i] == "<": - i = skip_whitespace(xml_string, i + 1) - if xml_string[i] == "/": - tag, i, _ = parse_tag(xml_string, i + 1) - else: - tag, i, attr_value_pairs = parse_tag(xml_string, i) - if attr_value_pairs: - for attr, value in attr_value_pairs: + xml_string = prune_starting(xml_string) + + try: + while i < len(xml_string): + i = skip_whitespace(xml_string, i) + if i >= len(xml_string): + break + if xml_string[i] == '<': + i = skip_whitespace(xml_string, i + 1) + if xml_string[i] == '/': + tag, i, _ = parse_tag(xml_string, i + 1) + else: + tag, i, attr_value_pairs = parse_tag(xml_string, i) + if attr_value_pairs: + for _, value in attr_value_pairs: + add_val(tag, value) + tags += [tag] # Attrs only need one tag + else: + value, i = parse_value(xml_string, i) add_val(tag, value) - tags += [tag] # Attrs only need one tag - else: - value, i = parse_value(xml_string, i) - add_val(tag, value) - tags += [tag] - else: - while i < len(xml_string) and xml_string[i] != "<": - i += 1 + tags += [tag] + else: + while i < len(xml_string) and xml_string[i] != '<': + i += 1 + + return parse_tags(tags, storage) + except BaseException as exc: + raise ParsingError( + 'ERROR: Unable to parse response into XML format!') from exc - return parse_tags(tags, storage) + @staticmethod + def parse(text: str): + return XML.parse_xml(text) diff --git a/grammarflow/prompt/__init__.py b/grammarflow/prompt/__init__.py index 0fc7d39..d07404d 100644 --- a/grammarflow/prompt/__init__.py +++ b/grammarflow/prompt/__init__.py @@ -1 +1 @@ -from .builder import Prompt, PromptBuilder \ No newline at end of file +from .builder import Prompt, PromptBuilder diff --git a/grammarflow/prompt/builder.py b/grammarflow/prompt/builder.py index 0516548..8cf53c6 100644 --- a/grammarflow/prompt/builder.py +++ b/grammarflow/prompt/builder.py @@ -1,10 +1,21 @@ from grammarflow.grammars.json import JSON from grammarflow.grammars.toml import TOML from grammarflow.grammars.xml import XML +from grammarflow.grammars.error import ConfigError + +from typing import Dict, List, Any class Prompt: - def __init__(self, built_prompt, placeholders=[], stop_at=""): + """ + Dataclass to store the built prompt. + """ + + def __init__( + self, + built_prompt: str, + placeholders: List = None, + stop_at: str = ""): self.placeholders = placeholders self.prompt = built_prompt self.stop_at = stop_at @@ -21,6 +32,12 @@ def fill(self, **kwargs): class PromptBuilder: + """ + Interface to build Prompts + Allows for building prompts with multiple sections, placeholders, grammars, and examples. + Use `enable_on` to conditionally enable sections based on the inputs provided. + """ + def __init__(self): self.sections = [] self.grammar_set = False @@ -28,23 +45,43 @@ def __init__(self): self.placeholders = [] self.stop_at = "" + # pylint: disable=missing-function-docstring def add_section( - self, text="", placeholders=[], define_grammar=False, remind_grammar=False, add_few_shot_examples=[], enable_on=None - ): - type_ = "fixed" + self, + text: str = "", + placeholders: List = None, + define_grammar: bool = False, + remind_grammar: bool = False, + add_few_shot_examples: List = None, + enable_on: Any = None): + assert isinstance(text, str), "`text` needs to be a `str` type" + + if placeholders: + if isinstance(placeholders, str): + placeholders = [placeholders] + assert isinstance( + placeholders, list), "`placeholders` needs to be a `list` type" + + # Marking which sections can be edited by grammarflow if placeholders and not enable_on: self.placeholders.extend(placeholders) type_ = "editable" + else: + type_ = "fixed" - if self.grammar_set and define_grammar: - raise ValueError("Only one section can define the grammar.") + if self.grammar_set and define_grammar: # Only one section can define the grammar + raise ConfigError("Only one section can define the grammar.") + + # Reminders were an experimental section, read below. TODO: [Add link + # from below here.] if remind_grammar and not self.grammar_set: - raise ValueError("You cannot remind about the grammar without defining it.") + raise ConfigError( + "You cannot remind about the grammar without defining it.") if define_grammar: - self.grammar_set = True + self.grammar_set = True # We want grammar to be defined here if add_few_shot_examples: - self.examples_set = True + self.examples_set = True # We want examples to be added here self.sections.append( { @@ -54,84 +91,107 @@ def add_section( "define_grammar": define_grammar, "remind_grammar": remind_grammar, "examples": add_few_shot_examples, - "enable_on": enable_on + "enable_on": enable_on } ) - def get_text(self): + def get_text(self): # pylint: disable=missing-function-docstring return " ".join([section["text"] for section in self.sections]) - def build(self, config): + def build(self, config: Dict) -> Prompt: + ''' + Builds the prompt using the current `config`. Not stateful. + ''' + prompt = "" - user_prompt = "" for section in self.sections: - # section["enable_on"] is meant to be a function + # section["enable_on"] must be a function if section["enable_on"] and not config['enable_on']: - raise ValueError("You need to provide the inputs to the enable_on functions you've defined in PromptBuilder.") + raise ConfigError( + "You need to provide the inputs to the `enable_on` functions you've defined in PromptBuilder.") elif section["enable_on"] and config['enable_on']: if not section["enable_on"](**config['enable_on']): continue - else: + else: self.placeholders.extend(section["placeholders"]) - grammar_instruction = "" + grammar_instruction, grammar, reminders = "", "", [] + section_text = section["text"] + # If the section is supposed to define the grammar, we need to + # extract the grammar from the config if section["define_grammar"]: if "grammars" in config: grammar_instruction, grammar, reminders = self.make_format( - config.get("format"), config["grammars"], config["return_sequence"] - ) + config["grammars"], config.get("format"), config["return_sequence"]) + # If it's fixed, we don't do anything, except grammar addition. if section["type"] == "fixed": - section_text = section["text"] if grammar_instruction: - section_text += "\n" + grammar_instruction + "\n\n" + grammar + reminders[-1] + section_text += "\n" + grammar_instruction + \ + "\n\n" + grammar + reminders[-1] + # If it's editable, there will be placeholders we can play with. + # grammar addition can happen here too, but will be placed after + # placeholder is replaced. elif section["type"] == "editable": - if section["placeholders"] and grammar_instruction: + if section["placeholders"]: for placeholder in section["placeholders"]: - section["text"] = section["text"].replace( - f"{{{placeholder}}}", f"{{{placeholder}}}\n" + grammar_instruction + "\n\n" + grammar + reminders[-1] - ) - section_text = section["text"] - + if grammar: + section_text = section_text.replace( + f"{{{placeholder}}}", f"{{{placeholder}}}\n" + grammar_instruction + "\n\n" + grammar + reminders[-1] + ) + else: + section_text = section_text.replace( + f"{{{placeholder}}}", f"{{{placeholder}}}\n" + ) + if section["examples"] and config["examples"]: - _, grammar, _ = self.make_format(config.get("format"), config["examples"], None) - section_text += "\nHere are some examples:\n" + _, grammar, _ = self.make_format( + config["examples"], config.get("format"), None) + section_text += "\n Here is an example:\n" section_text += f"{grammar}\n" - if section_text: prompt += section_text + "\n" + if section_text: + prompt += section_text + "\n" + + return Prompt( + prompt.strip(), + placeholders=self.placeholders, + stop_at=self.stop_at) - return Prompt(prompt.strip(), placeholders=self.placeholders, stop_at=self.stop_at) + def make_format(self, + grammars: Dict, + serialization_type: str = 'json', + return_sequence: str = 'single_response') -> List[Any]: - def make_format(self, serialization_type, grammars, return_sequence): + # Decide which formatter if serialization_type == "json": formatter = JSON elif serialization_type == "toml": formatter = TOML elif serialization_type == "xml": formatter = XML - else: - formatter = JSON grammar, model_names = formatter.make_format(grammars, return_sequence) if model_names: response_type = "ONLY" if return_sequence == "single_response" else "ALL OF" - newline = "\\n" + + # `reminders` was an idea to test how repeated reminders would help with increasing context size + # did not find improvement, but keeping it here for future + # reference if len(model_names) > 1: - instruction = f"Here are the custom output formats you are expected to return your responses in" - reminders = "\nRETURN {} {}. TERMINALS = Cover output with '***'; End lines with newline. \n".format( - response_type, ", ".join(model_names), newline - ) + instruction = "Here are the custom output formats you are expected to return your responses in" + reminders = f"\nRETURN {response_type} {", ".join( + model_names)}. TERMINALS = Cover output with '```'; End lines with '\n'. \n" else: - instruction = f"Here is the custom output format you are expected to return your response in." - reminders = "\nRETURN {} ONE OF {}. TERMINALS = Cover output with '***'; End lines with newline. \n".format( - response_type, ", ".join(model_names), newline - ) + instruction = "Here is the custom output format you are expected to return your response in." + reminders = f"\nRETURN {response_type} ONE OF {", ".join( + model_names)}. TERMINALS = Cover output with '```'; End lines with '\n'. \n" else: instruction = None reminders = None - return instruction, grammar, [reminders] \ No newline at end of file + return instruction, grammar, [reminders] diff --git a/grammarflow/prompt/template.py b/grammarflow/prompt/template.py index 740566f..78d93e6 100644 --- a/grammarflow/prompt/template.py +++ b/grammarflow/prompt/template.py @@ -1,81 +1,122 @@ -from .builder import Prompt, PromptBuilder +from .builder import PromptBuilder -def Llama2Instruct(size='70B'): - if size != '70B': +def Llama2Instruct(size='70B'): # pylint: disable=missing-function-docstring + if size != '70B': + prompt = PromptBuilder() + prompt.add_section( + text="[INST] <>\n{instructions}", + placeholders=["instructions"], + define_grammar=True) + prompt.add_section(add_few_shot_examples=True) + prompt.add_section(text="<>") + prompt.add_section(text="{prompt}[/INST]", placeholders=["prompt"]) + prompt.stop_at = "[/INST]" + else: + prompt = PromptBuilder() + prompt.add_section( + text="### System:\n{instructions}\n", + placeholders=["instructions"], + define_grammar=True) + prompt.add_section(add_few_shot_examples=True) + prompt.add_section( + text="### User:\n{prompt}\n", + placeholders=["prompt"]) + prompt.add_section(text="### Assistant:\n") + prompt.stop_at = "### Assistant:" + return prompt + + +def Mistral(**kwargs): prompt = PromptBuilder() - prompt.add_section(text="[INST] <>\n{instructions}", placeholders=["instructions"], define_grammar=True) + prompt.add_section( + text="[INST] {instructions}", + placeholders=['instructions'], + define_grammar=True) prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="<>") - prompt.add_section(text="{prompt}[/INST]", placeholders=["prompt"]) + prompt.add_section(text="{prompt} [/INST]", placeholders=['prompt']) prompt.stop_at = "[/INST]" - else: + return prompt + + +def Mixtral(**kwargs): + prompt = PromptBuilder() + prompt.add_section( + text="SYSTEM: {instructions}", + placeholders=['instructions'], + define_grammar=True) + prompt.add_section(text="USER: {prompt}", placeholders=['prompt']) + prompt.add_section(add_few_shot_examples=True) + prompt.add_section(text="ASSISTANT:") + return prompt + + +def ChatML(**kwargs): # pylint: disable=missing-function-docstring + prompt = PromptBuilder() + prompt.add_section( + text="<|im_start|>system\n{system_message}", + placeholders=['system_message'], + define_grammar=True) + prompt.add_section(add_few_shot_examples=True) + prompt.add_section(text="<|im_end|>") + prompt.add_section( + text="<|im_start|>user\n{prompt}<|im_end|>", + placeholders=['prompt']) + prompt.add_section(text="<|im_start|>assistant") + return prompt + + +def ZeroShot(): + prompt = PromptBuilder() + prompt.add_section( + text="Your role: {instructions}", + placeholders=['instructions']) + prompt.add_section(define_grammar=True) + prompt.add_section(text="{prompt}", placeholders=['prompt']) + return prompt + + +def FewShot(): + prompt = PromptBuilder() + prompt.add_section( + text="Your role: {instructions}", + placeholders=['instructions']) + prompt.add_section(define_grammar=True) + prompt.add_section(add_few_shot_examples=True) + prompt.add_section(text="{prompt}", placeholders=['prompt']) + return prompt + + +def Agent(enable): + # pylint: disable=missing-function-docstring + assert callable(enable), "`enable` parameter must be a function! Read up on `enable_on` option in PromptBuilder from the documentation." + + prompt = PromptBuilder() + prompt.add_section( + text="Your role: {instructions}", + placeholders=['instructions']) + prompt.add_section(define_grammar=True) + prompt.add_section(text="Your goal: {prompt}", placeholders=['prompt']) + prompt.add_section( + text="Below is the history of the conversation so far: {history}", + placeholders=['history'], + enable_on=enable) + return prompt + + +def ChainOfThought(**kwargs): + prompt = PromptBuilder() + prompt.add_section(text="You are a professor of various practices. You like to think through the steps you take in solving your goal. You will be presented with a 'goal'. Solve it by iteratively making observations.") + prompt.add_section(define_grammar=True) + prompt.add_section(text="Your 'goal': {prompt}", placeholders=['prompt']) + return prompt + + +def Instruction(): prompt = PromptBuilder() - prompt.add_section(text="### System:\n{instructions}\n", placeholders=["instructions"], define_grammar=True) + prompt.add_section( + text="Here are your Instructions: {instructions}", + placeholders=['instructions']) + prompt.add_section(define_grammar=True) prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="### User:\n{prompt}\n", placeholders=["prompt"]) - prompt.add_section(text="### Assistant:\n") - prompt.stop_at = "### Assistant:" - return prompt - -def Mistral(**kwargs): - prompt = PromptBuilder() - prompt.add_section(text="[INST] {instructions}", placeholders=['instructions'], define_grammar=True) - prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="{prompt} [/INST]", placeholders=['prompt']) - prompt.stop_at = "[/INST]" - return prompt - -def Mixtral(**kwargs): - prompt = PromptBuilder() - prompt.add_section(text="SYSTEM: {instructions}", placeholders=['instructions'], define_grammar=True) - prompt.add_section(text="USER: {prompt}", placeholders=['prompt']) - prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="ASSISTANT:") - return prompt - -def ChatML(**kwargs): - prompt = PromptBuilder() - prompt.add_section(text="<|im_start|>system\n{system_message}", placeholders=['system_message'], define_grammar=True) - prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="<|im_end|>") - prompt.add_section(text="<|im_start|>user\n{prompt}<|im_end|>", placeholders=['prompt']) - prompt.add_section(text="<|im_start|>assistant") - return prompt - -def ZeroShot(): - prompt = PromptBuilder() - prompt.add_section(text="Your role: {instructions}", placeholders=['instructions']) - prompt.add_section(define_grammar=True) - prompt.add_section(text="{prompt}", placeholders=['prompt']) - return prompt - -def FewShot(): - prompt = PromptBuilder() - prompt.add_section(text="Your role: {instructions}", placeholders=['instructions']) - prompt.add_section(define_grammar=True) - prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="{prompt}", placeholders=['prompt']) - return prompt - -def Agent(enable): - prompt = PromptBuilder() - prompt.add_section(text="Your role: {instructions}", placeholders=['instructions']) - prompt.add_section(define_grammar=True) - prompt.add_section(text="Your goal: {prompt}", placeholders=['prompt']) - prompt.add_section(text="Below is the history of the conversation so far: {history}", placeholders=['history'], enable_on=enable) - return prompt - -def ChainOfThought(**kwargs): - prompt = PromptBuilder() - prompt.add_section(text="You are a professor of various practices. You like to think through the steps you take in solving your goal. You will be presented with a 'goal'. Solve it by iteratively making observations.") - prompt.add_section(define_grammar=True) - prompt.add_section(text="Your 'goal': {prompt}", placeholders=['prompt']) - return prompt - -def Instruction(): - prompt = PromptBuilder() - prompt.add_section(text="Here are your Instructions: {instructions}", placeholders=['instructions']) - prompt.add_section(define_grammar=True) - prompt.add_section(add_few_shot_examples=True) - prompt.add_section(text="{prompt}", placeholders=['prompt']) - return prompt + prompt.add_section(text="{prompt}", placeholders=['prompt']) + return prompt diff --git a/grammarflow/tools/__init__.py b/grammarflow/tools/__init__.py index 0204f06..23f7b89 100644 --- a/grammarflow/tools/__init__.py +++ b/grammarflow/tools/__init__.py @@ -1,2 +1,2 @@ from .pydantic import ModelParser -from .response import Response \ No newline at end of file +from .response import Response diff --git a/grammarflow/tools/llm.py b/grammarflow/tools/llm.py index bca6a28..ab9951f 100644 --- a/grammarflow/tools/llm.py +++ b/grammarflow/tools/llm.py @@ -1,22 +1,34 @@ -import openai -import os -import sys +from openai import OpenAI +import os +import sys import subprocess from stopit import threading_timeoutable as timeoutable +from typing import Dict -class OpenAI: - def __init__(self, model_name='gpt-3.5-turbo'): - self.client = OpenAI( - api_key=os.environ["OPENAI_API_KEY"], - ) - def __call__(self, prompt, temperature=0.1): - response = self.client.chat.completions.create( - model=model_name, - messages=[{"role": "user", "content": prompt}], - temperature=temperature, - ) - return response.choices[0].message.content +class OpenAI: # pylint: disable=missing-function-docstring + def __init__(self, model_name: str = 'gpt-3.5-turbo'): + self.client = OpenAI( + api_key=os.environ["OPENAI_API_KEY"], + ) + self.model_name = model_name + + def __call__(self, prompt: str, temperature: float = 0.1) -> str: + """ + Args: + prompt (str): The prompt to be passed to the model. + temperature (float): Higher the temperature, the more creative the output. Default is 0.1. + """ + + response = self.client.chat.completions.create( + model=self.model_name, + messages=[{"role": "user", "content": prompt}], + temperature=temperature, + ) + return response.choices[0].message.content + +# Taken from +# https://stackoverflow.com/questions/11130156/suppress-stdout-stderr-print-from-python-functions class suppress_stdout_stderr(object): @@ -24,70 +36,101 @@ def __enter__(self): self.outnull_file = open(os.devnull, 'w') self.errnull_file = open(os.devnull, 'w') - self.old_stdout_fileno_undup = sys.stdout.fileno() - self.old_stderr_fileno_undup = sys.stderr.fileno() + self.old_stdout_fileno_undup = sys.stdout.fileno() + self.old_stderr_fileno_undup = sys.stderr.fileno() - self.old_stdout_fileno = os.dup ( sys.stdout.fileno() ) - self.old_stderr_fileno = os.dup ( sys.stderr.fileno() ) + self.old_stdout_fileno = os.dup(sys.stdout.fileno()) + self.old_stderr_fileno = os.dup(sys.stderr.fileno()) self.old_stdout = sys.stdout self.old_stderr = sys.stderr - os.dup2 ( self.outnull_file.fileno(), self.old_stdout_fileno_undup ) - os.dup2 ( self.errnull_file.fileno(), self.old_stderr_fileno_undup ) + os.dup2(self.outnull_file.fileno(), self.old_stdout_fileno_undup) + os.dup2(self.errnull_file.fileno(), self.old_stderr_fileno_undup) - sys.stdout = self.outnull_file + sys.stdout = self.outnull_file sys.stderr = self.errnull_file return self - def __exit__(self, *_): + def __exit__(self, *_): sys.stdout = self.old_stdout sys.stderr = self.old_stderr - os.dup2 ( self.old_stdout_fileno, self.old_stdout_fileno_undup ) - os.dup2 ( self.old_stderr_fileno, self.old_stderr_fileno_undup ) + os.dup2(self.old_stdout_fileno, self.old_stdout_fileno_undup) + os.dup2(self.old_stderr_fileno, self.old_stderr_fileno_undup) - os.close ( self.old_stdout_fileno ) - os.close ( self.old_stderr_fileno ) + os.close(self.old_stdout_fileno) + os.close(self.old_stderr_fileno) self.outnull_file.close() self.errnull_file.close() -class LocalLlama: - def __init__(self, gguf_path: str, llama_cpp_path=os.environ['LLAMA']): - self.llama_cpp_path = llama_cpp_path - self.gguf_path = gguf_path - self.flags = { - "repeat_penalty": 1.5, - "n-gpu-layers": 15000, - 'ctx_size': 2048 - } - - @timeoutable() - def __call__(self, prompt: str, flags=None, grammar=None, stop_at="", temperature=0.1, timeout=50): - if flags: - self.flags.update(flags) - - flags = self.flags - - if grammar: - with open('./grammar.gnbf', 'w') as f: - f.write(grammar) - self.flags.update({'grammar-file': './grammar.gnbf'}) - - self.write_file(prompt) - - with suppress_stdout_stderr(): - output = subprocess.check_output(self.format_command(prompt, flags), shell=True).decode('utf-8') - - if stop_at: - return output.split(stop_at)[1] - - return output - - def write_file(self, prompt): - with open('./prompt.txt', 'w') as f: - f.write(prompt) - def format_command(self, prompt: str, flags, temperature=0.1): - return f"{self.llama_cpp_path}/main --model {self.gguf_path} {" ".join([f"--{k} {v}" for k, v in flags.items()])} --file ./prompt.txt --temp {temperature}" \ No newline at end of file +class LocalLlama: + def __init__( + self, + gguf_path: str, + llama_cpp_path: str = os.environ['LLAMA']): + """ + Initializes a barebones interface between user and llama.cpp + Why? llama-cpp-python is quite slow. Discussion here: https://www.reddit.com/r/LocalLLaMA/comments/14evg0g/llamacpppython_is_slower_than_llamacpp_by_more/ + """ + + self.llama_cpp_path = llama_cpp_path + self.gguf_path = gguf_path + self.flags = { + "repeat_penalty": 1.5, + "n-gpu-layers": 15000, + 'ctx_size': 2048 + } + + @timeoutable() + def __call__( + self, + prompt: str, + flags: Dict = None, + grammar: str = None, + stop_at: str = "", + temperature: float = 0.1, + timeout: int = 50) -> str: + """ + Args: + prompt (str): The prompt to be passed to the model. + flags (Dict): Flags to be passed to the model. + grammar (str): The grammar to be passed to the model. Needs to be a string in GNBF format. + stop_at (str): The token at which generation should be stopped. + temperature (float): The temperature to be passed to the model. + timeout (int): The timeout for the model. Default is 50 seconds. + Returns: + str: The output from the model. + """ + + if flags: + self.flags.update(flags) + + flags = self.flags + + if grammar: + with open('./grammar.gnbf', 'w') as f: + f.write(grammar) + self.flags.update({'grammar-file': './grammar.gnbf'}) + + self.write_file(prompt) + + with suppress_stdout_stderr(): + output = subprocess.check_output( + self.format_command( + prompt, flags), shell=True).decode('utf-8') + + if stop_at: + return output.split(stop_at)[1] + + return output + + def write_file(self, prompt): + with open('./prompt.txt', 'w') as f: + f.write(prompt) + + def format_command(self, prompt: str, flags, temperature=0.1): + return f"{self.llama_cpp_path}/main --model {self.gguf_path} {" ".join( + [f"--{k} {v}" for k, v in flags.items()])} --file ./prompt.txt --temp {temperature}" diff --git a/grammarflow/tools/pydantic.py b/grammarflow/tools/pydantic.py index b9fd4f5..5be19b6 100644 --- a/grammarflow/tools/pydantic.py +++ b/grammarflow/tools/pydantic.py @@ -1,18 +1,22 @@ from typing import Any, Dict, List, Optional, Type - from pydantic import BaseModel - class EmptyModelField(BaseModel): default: Optional[str] = None required: Optional[bool] = True description: Optional[str] = None value: Optional[str] = None - class ModelParser: @staticmethod - def screen_field_info(model: BaseModel): + def screen_field_info(model: BaseModel) -> Dict[str, Dict[str, Any]]: # pylint: disable=missing-function-docstring + """ + Extracts field information from a Pydantic model (not schema). + + Args: + model (BaseModel): The Pydantic model. + """ + temp = {} for field_name, field_info in model.__fields__.items(): @@ -26,50 +30,70 @@ def screen_field_info(model: BaseModel): try: temp[field_name]["description"] = field_info.field_info.description - except: + except BaseException: try: temp[field_name]["description"] = field_info.description - except: + except BaseException: pass try: temp[field_name]["value"] = getattr(model, field_name) - except: + except BaseException: pass return temp @staticmethod - def screen_model_schema(schema: dict): + def screen_model_schema(schema: Dict) -> Dict[str, Dict[str, str]]: + """ + Extracts fields with descriptions from a Pydantic model schema. + + Args: + schema (Dict): The schema of a Pydantic model; get using model.schema() + """ + temp = {} for field_name in schema["properties"]: - temp[field_name] = {} if "anyOf" in schema["properties"][field_name]: - temp[field_name]["type"] = " OR ".join([x["type"] for x in schema["properties"][field_name]["anyOf"]]) + temp[field_name]["type"] = " OR ".join( + [x["type"] + for x in schema["properties"][field_name]["anyOf"]]) elif "items" in schema["properties"][field_name]: if "$ref" in schema["properties"][field_name]["items"]: - nested_model_name = schema["properties"][field_name]["items"]["$ref"].split("/")[-1] + nested_model_name = schema["properties"][field_name][ + "items"]["$ref"].split("/")[-1] temp[field_name]["type"] = "List[" + nested_model_name + "]" else: - temp[field_name]["type"] = "List[" + schema["properties"][field_name]["items"]["type"] + "]" + temp[field_name]["type"] = "List[" + \ + schema["properties"][field_name]["items"]["type"] + "]" elif "$ref" in schema["properties"][field_name]: - temp[field_name]["type"] = schema["properties"][field_name]["$ref"].split("/")[-1] + temp[field_name]["type"] = schema["properties"][field_name][ + "$ref"].split("/")[-1] else: temp[field_name]["type"] = schema["properties"][field_name]["type"] - temp[field_name]["type"] = temp[field_name]["type"].replace("string", '"string"') + temp[field_name]["type"] = temp[field_name]["type"].replace( + "string", '"string"') - temp[field_name]["description"] = schema["properties"][field_name].get("description", None) + temp[field_name]["description"] = schema["properties"][field_name].get( + "description", None) + temp[field_name]["pattern"] = schema["properties"][field_name].get( + "pattern", None) return temp @staticmethod - def extract_fields_with_descriptions(model_classes: List[Type[BaseModel]]) -> Dict[str, Dict[str, Dict[str, str]]]: + def extract_fields_with_descriptions( + model_classes: List[Type[BaseModel]]) -> Dict[str, Dict[str, Dict[str, str]]]: """ - Extracts and returns a dictionary containing descriptions of fields - from a list of Pydantic model classes. + Extracts fields with descriptions from a list of Pydantic models. + + Args: + model_classes (List[Type[BaseModel]]): List of Pydantic models. + Returns: + A dictionary with model names as keys and field names as keys of the inner dictionary. The inner dictionary contains field descriptions. """ def update_dict(d1, d2): @@ -85,15 +109,19 @@ def update_dict(d1, d2): fields = {} nested = 0 - for count, model in enumerate(model_classes): + for _, model in enumerate(model_classes): temp = {} schema = model.schema() if "definitions" in schema: nested = 1 - for nested_model_name, nested_model_schema in schema["definitions"].items(): - fields[nested_model_name] = ModelParser.screen_model_schema(nested_model_schema) + for nested_model_name, nested_model_schema in schema["definitions"].items( + ): + fields[nested_model_name] = ModelParser.screen_model_schema( + nested_model_schema) + # I'm processing the schema and model separately + # Sometimes, the schema doesn't contain all the information I want (issue with pydantic versions and changes) update_dict(temp, ModelParser.screen_model_schema(schema)) update_dict(temp, ModelParser.screen_field_info(model)) diff --git a/grammarflow/tools/response.py b/grammarflow/tools/response.py index 740cc6a..c828d99 100644 --- a/grammarflow/tools/response.py +++ b/grammarflow/tools/response.py @@ -1,7 +1,13 @@ import json +from typing import Dict, List, Union class Response: + """ + Dataclass to store the parsed response. + Does not enforce type checking, but provides a schema method to extract the schema of the response. + """ + def __init__(self, data=None): if isinstance(data, (dict, list)): if isinstance(data, list): @@ -11,36 +17,39 @@ def __init__(self, data=None): else: self._data = {} - def __getattr__(self, name): - if isinstance(self._data, dict) and name in self._data: - if isinstance(self._data[name], (dict, list)): - return Response(self._data[name]) - return self._data[name] + def __getattr__(self, key: str): + if isinstance(self._data, dict) and key in self._data: + if isinstance(self._data[key], (dict, list)): + return Response(self._data[key]) + return self._data[key] return None - def __setattr__(self, name, value): - if name == "_data": - super().__setattr__(name, value) + def __setattr__(self, key: str, value: Union[Dict, List, str]): + if key == "_data": + super().__setattr__(key, value) else: - self._data[name] = value + if isinstance(value, str): + value = value.replace('\\n', '\n') + self._data[key] = value - def __getitem__(self, key): + def __getitem__(self, key: str): if (isinstance(self._data, list) and isinstance(key, int)) or ( isinstance(self._data, dict) and key in self._data ): return self._data[key] return None - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Union[Dict, List]): if isinstance(self._data, dict) or isinstance(self._data, list): self._data[key] = value else: - raise TypeError("Assignment not supported for uninitialized Response objects.") + raise TypeError( + "Assignment not supported for uninitialized Response objects.") def __repr__(self): return repr(self._data) - def schema(self, path="", depth=0): + def schema(self, path: str = "", depth: int = 0) -> Dict: # pylint: disable=missing-function-docstring schema_dict = {} if isinstance(self._data, dict): for k, v in self._data.items(): @@ -56,11 +65,10 @@ def schema(self, path="", depth=0): schema_dict = item_schema else: schema_dict = type(self._data).__name__ - return schema_dict - def __str__(self): - try : + def __str__(self) -> str: + try: return json.dumps(self._data, indent=4) - except: - return str(self._data) \ No newline at end of file + except BaseException: + return str(self._data) diff --git a/guide.ipynb b/guide.ipynb new file mode 100644 index 0000000..6330971 --- /dev/null +++ b/guide.ipynb @@ -0,0 +1,1041 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "from pydantic import BaseModel, Field\n", + "from typing import Any, List, Tuple, Type, Optional, Union\n", + "import os\n", + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dotenv import load_dotenv, find_dotenv\n", + "\n", + "load_dotenv(find_dotenv())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper Functions" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class LLM:\n", + " def __init__(self):\n", + " self.client = OpenAI(\n", + " api_key=os.environ[\"OPENAI_API_KEY\"],\n", + " )\n", + "\n", + " def request(self, prompt, temperature=0.2, context=None):\n", + " response = self.client.chat.completions.create(\n", + " model=\"gpt-3.5-turbo\",\n", + " messages=[{\"role\": \"user\", \"content\": prompt}],\n", + " temperature=temperature,\n", + " )\n", + " return response.choices[0].message.content\n", + "\n", + "\n", + "llm = LLM()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def log(msg):\n", + " print(msg)\n", + " print(\"-------------\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to make Prompts? " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from grammarflow.prompt.builder import PromptBuilder # Prompt builder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Prompts can be supplied to the `Constrain` block (below) as a string, `Prompt` or `PromptBuilder` object. \n", + "\n", + "`PromptBuilder` is a straightforward interface to build prompts, which are built using 'sections' which are user-defined. You can specificy which sections of the prompt can be conditionally switched on/off, which can be edited using placeholders, which are fixed sections, where the grammars can be defined, etc.\n", + "\n", + "Each prompt can have one `define_grammar`, `placeholders` and `add_few_shot_examples` boolean fields. These switch on certain functionalities within the sections. You can pass in a function to the `enable_on` field for the conditional enabling. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Sample Llama Prompt\n", + "\n", + "llama_prompt = PromptBuilder()\n", + "llama_prompt.add_section(\n", + " text=\"[INST] <>\\n{system_context}\\n<>\",\n", + " placeholders=[\"system_context\"],\n", + " define_grammar=True,\n", + ")\n", + "llama_prompt.add_section(\n", + " text=\"{user_message}[/INST]\",\n", + " placeholders=[\"user_message\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### How to use `enable_on`: \n", + "\n", + "Say we are working on a conversation chatbot, a chain that needs history for context. We might only want certain sections to be enabled when we have a history. We can use `enable_on` to enable or disable sections based on the presence of a history." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Dummy Example \n", + "\n", + "def check_previous_interaction(id_): return id_ > 1\n", + "\n", + "sample_prompt = PromptBuilder() \n", + "sample_prompt.add_section(\n", + " text=\"You are a intelligent search machine. Your goal is to think about what topics to search about to provide the user with relevant information. Here is his question: {question}\",\n", + " placeholders=['question']\n", + ") \n", + "sample_prompt.add_section(\n", + " define_grammar=True\n", + ")\n", + "sample_prompt.add_section(\n", + " text=\"Choose keywords from the context given below: \\n{history}\",\n", + " placeholders=[\"history\"],\n", + " enable_on=check_previous_interaction\n", + ") " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to make Grammars? \n", + "\n", + "Using Pydantic, we can define how data should be in pure python. GrammarFlow takes care of the rest. In this guide, you will find multiple examples of pydantic models for different use-cases. (Some of them are quite random, but I'm using them to prove effectiveness!)\n", + "\n", + "Here are some important rules! \n", + "\n", + "1. You can use Optional from `typing`, but the LLM won't understand when and when not to output an optional field. From experience, there are better ways to deal with Optional fields, such as enabling it within the grammar when needed. \n", + "2. When you want to use regex, you can do it using `Field(..., pattern=\"\", description=\"\")`. The `pattern` field will be used during decoding, but is ignored during prompt embedding. This is because LLMs cannot 100% perform regex handling unless explained in human-terms. Moreover, when you see LLMs conforming to the regex expectations, its mostly during the token sampling that it is achieved. So, to overcome this, you can embed a semantic explanation of the regex, like `Field(..., pattern=\"^(Akshath|Raghav|Ravikiran)$\", description=\"Akshath OR Raghav OR Ravikiran\")`.\n", + "3. Avoid using `Dict` in the pydantic model directly. Instead, make another model with the Dict fields and add that as a parameter to the original model. This becomes a nested structure, and can be handled well. If you choose `Dict[str, str]` method, then all I can do is put this into the prompt, which is ineffective and random. \n", + "\n", + "### Important note on acceptable regex for GNBF! \n", + "\n", + "Ensure your regex is solely `string` or `number` constraining. \n", + "> Example: `'(\"Akshath\"|\"Raghav\"|\"Ravikiran\")'` for multiple OR scenarios; `'\"https://\"[0-9a-fA-F]*'` for links, etc. \n", + "\n", + "If you want a `string` to be present, enclose it in double-qoutes. To allow for any other sequences, bound with `()` or `[]` and specify directly (`[0-9a-fA-F]*`). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Constraining with GrammarFlow" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from grammarflow.constrain import Constrain # Main class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A `Constrain` block acts as a context manager. It keeps track of the serialization type and the #grammars you want to handle. \n", + "\n", + "It offers three main functions. \n", + "1. `.format()` to format a prompt of your choice. Pass in `placeholders` as present in the format {'placeholder': text}. `grammars` needs to be a list of {'description': , 'model': }. `examples` needs to be a list of {'query': , 'model'}. \n", + "2. `.get_grammar()` to get the corresponding GNBF grammar of the model you pass in. \n", + "3. `.parse()` to parse the response into a `Response` dataclass. \n", + "4. `.inflation_rate()` can be used to get the token size at which the latest prompt has been increased. For smaller prompts, the number is >4x. For larger prompts, the number is <2x. You will find that **with increase of prompt size, the cost you will pay per LLM call will remain the same.**" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "user_message = \"\"\n", + "system_context = \"\"\n", + "\n", + "\n", + "class Model(BaseModel):\n", + " model_name: str\n", + "\n", + "\n", + "with Constrain('json') as manager:\n", + " prompt = manager.format(llama_prompt, \n", + " placeholders={'user_message': user_message, 'system_context': system_context},\n", + " grammars=[{'model': [Model]}]\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Serialization Use-Cases! \n", + "\n", + "1. 'JSON' is the classic go-to. Can handle simple stuff, nested-models, complex-grammars, etc. However, a simple missing terminal ('\"', '{\") can break the sampling chain. \n", + "2. 'XML' is the safest go-to. Can handle all cases, except multiple grammar generation (see at the end of `Examples!` section). The use of starting and ending tags is handled easily by token sampling, and errors within naming/tag is handled by my parser. \n", + "3. 'TOML' is best when we want to get multiple grammars generated in one-go. With a smaller inflation rate of the prompt (before and after grammarflow embeddings), it can handle a longer list of fields. However, it *cannot* work for nested models. TOML nested models usually are in the form given below. From my experimentations, LLMs have a hard time conforming to this and end up generating `obj2` field within `obj1` as a `Dict` object. Is it possible? Sure. Would I trust it? No.\n", + " ```\n", + " [obj1]\n", + " field = \"\" \n", + " field2 = \"\" \n", + "\n", + " [obj1.obj2]\n", + " field3 = \"\" \n", + " ```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Examples! " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "```\n", + "\n", + " \"fib\" \n", + " \"This function returns the fibonacci sequence.\" \n", + " [\"numpy\"] \n", + " 123456789 \n", + " true \n", + " \"import numpy as np\\n\\ndef fib(n):\\n a, b = 0, 1\\n result = []\\n for _ in range(n):\\n result.append(a)\\n a, b = b, a + b\\n return np.array(result)\" \n", + "\n", + "```\n", + "-------------\n", + "{\n", + " \"FunctionModel\": {\n", + " \"function_name\": \"fib\",\n", + " \"docstring\": \"This function returns the fibonacci sequence.\",\n", + " \"depedencies\": [\n", + " \"numpy\"\n", + " ],\n", + " \"uuid\": 123456789,\n", + " \"is_python\": true,\n", + " \"code\": \"import numpy as np\\\\n\\\\ndef fib(n):\\\\n a, b = 0, 1\\\\n result = []\\\\n for _ in range(n):\\\\n result.append(a)\\\\n a, b = b, a + b\\\\n return np.array(result)\"\n", + " }\n", + "}\n", + "-------------\n", + "{'before': 27, 'after': 143, 'factor': '4.3x'}\n", + "-------------\n" + ] + } + ], + "source": [ + "# # Here's a simple example of asking an LLM to make code.\n", + "# # This can be used within coding assistants which requires extra metadata.\n", + "\n", + "\n", + "class FunctionModel(BaseModel):\n", + " function_name: str\n", + " docstring: str\n", + " depedencies: List[str]\n", + " uuid: Union[float, int]\n", + " is_python: bool\n", + " code: str\n", + "\n", + "\n", + "input_str = \"I want to create a function that returns the fibonacci sequence. The function should be called 'fib'. The function can use numpy.\"\n", + "\n", + "with Constrain('xml') as manager:\n", + " prompt = manager.format(input_str, grammars=[{\"description\": \"No Code Generation\", \"model\": FunctionModel}])\n", + "\n", + " llm_response = llm.request(prompt, temperature=0.01)\n", + " log(llm_response)\n", + "\n", + " response = manager.parse(llm_response)\n", + " log(response)\n", + "\n", + " log(manager.inflation_rate())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] + } + ], + "source": [ + "# # The response will be of `Response` type, which can be used to extract the data. If adding the parsed response to this object fails, it will return the dict itself.\n", + "print(response.FunctionModel.is_python)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "```\n", + "\n", + " fib \n", + " This function returns the fibonacci sequence. \n", + " ['numpy'] \n", + " 987654321.0 \n", + " True \n", + " def fib(n):\n", + "\tif n <= 1:\n", + "\t\treturn n\n", + "\telse:\n", + "\t\treturn fib(n-1) + fib(n-2) \n", + "\n", + "```\n", + "-------------\n", + "{\n", + " \"FunctionModel\": {\n", + " \"function_name\": \"fib\",\n", + " \"docstring\": \"This function returns the fibonacci sequence.\",\n", + " \"depedencies\": [\n", + " \"numpy\"\n", + " ],\n", + " \"uuid\": 987654321.0,\n", + " \"is_python\": true,\n", + " \"code\": \"def fib(n):\\tif n <= 1:\\t\\treturn n\\telse:\\t\\treturn fib(n-1) + fib(n-2)\"\n", + " }\n", + "}\n", + "-------------\n", + "{'before': 27, 'after': 237, 'factor': '7.8x'}\n", + "-------------\n" + ] + } + ], + "source": [ + "# Add some examples too!\n", + "\n", + "Sum_Function_Model = FunctionModel(\n", + " function_name=\"sum\",\n", + " docstring=\"This function returns the sum of the input list.\",\n", + " depedencies=[\"numpy\"],\n", + " uuid=123456789,\n", + " is_python=True,\n", + " code=\"def sum(a, b):\\n\\treturn a + b\"\n", + ")\n", + "\n", + "\n", + "with Constrain('xml') as manager:\n", + " prompt = manager.format(input_str, \n", + " grammars=[\n", + " {\n", + " 'description': 'No Code Generation',\n", + " 'model': FunctionModel\n", + " }\n", + " ],\n", + " examples=[\n", + " {\n", + " 'query': \"Create a summation function in Python\",\n", + " 'model': Sum_Function_Model\n", + " }\n", + " ]\n", + " )\n", + "\n", + "\n", + " llm_response = llm.request(prompt, temperature=0.01)\n", + " log(llm_response)\n", + "\n", + " response = manager.parse(llm_response)\n", + " log(response)\n", + "\n", + " log(manager.inflation_rate())" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "```\n", + "{\n", + "\"ThoughtState\": {\n", + "\"thought\": \"I need to gather information about Vladmir Putin.\",\n", + "\"goal\": \"To learn more about Vladmir Putin.\",\n", + "\"tool\": \"Web_Search\",\n", + "\"action\": \"Read\",\n", + "\"action_input\": \"Vladmir Putin\",\n", + "\"thought_id\": \"1a2b3c4d\"\n", + "}\n", + "}\n", + "```\n", + "-------------\n", + "{\n", + " \"ThoughtState\": {\n", + " \"thought\": \"I need to gather information about Vladmir Putin.\",\n", + " \"goal\": \"To learn more about Vladmir Putin.\",\n", + " \"tool\": \"Web_Search\",\n", + " \"action\": \"Read\",\n", + " \"action_input\": \"Vladmir Putin\",\n", + " \"thought_id\": \"1a2b3c4d\"\n", + " }\n", + "}\n", + "-------------\n", + "{'before': 48, 'after': 230, 'factor': '3.8x'}\n", + "-------------\n" + ] + } + ], + "source": [ + "# Sample ReAct Model with Llama Prompt\n", + "# You can add descriptions within the grammar model to provide it's context and options. This is how we use the LLM in https://github.com/e-lab/Forestry_Student/.\n", + "\n", + "class ThoughtState(BaseModel):\n", + " thought: str\n", + " goal: str\n", + " tool: str = Field(...,\n", + " description=\"Choose one of ['Web_QA', 'Web_Search', 'Web_Scraping', 'Web_Automation', 'Web_Research']\")\n", + " action: str = Field(...,\n", + " description=\"Choose one of ['Create', 'Update', 'Delete', 'Read']\")\n", + " action_input: str = Field(..., description=\"The input data for the action\")\n", + " thought_id: Optional[str] = Field(\n", + " None, description=\"The unique identifier for the thought\")\n", + "\n", + "\n", + "system_context = \"\"\"Your goal is to think and plan out how to solve questions using agent tools provided to you. Think about all aspects of your thought process.\"\"\"\n", + "user_message = \"\"\"Who is Vladmir Putin?\"\"\"\n", + "\n", + "with Constrain('json') as manager:\n", + " prompt = manager.format(llama_prompt, placeholders={\n", + " 'user_message': user_message,\n", + " 'system_context': system_context\n", + " },\n", + " grammars=[{\n", + " 'description': 'This format describes your current thinking state',\n", + " 'model': [ThoughtState]}]\n", + " )\n", + "\n", + " llm_response = llm.request(prompt, temperature=0.01)\n", + " log(llm_response)\n", + "\n", + " response = manager.parse(llm_response)\n", + " log(response)\n", + "\n", + " log(manager.inflation_rate())" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Web_Search\n" + ] + } + ], + "source": [ + "# # You can then access the response from the `response` object\n", + "print(response.ThoughtState.tool)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "```\n", + "{\n", + "\"Project\": {\n", + "\"name\": \"Multimodal Document Understanding Project\",\n", + "\"description\": \"A project focused on developing a system for understanding documents using multiple modes of input such as text, images, and audio.\",\n", + "\"project_url\": \"https://example.com/multimodal-document-understanding\",\n", + "\"team_members\": [\n", + "{\n", + "\"name\": \"John Doe\",\n", + "\"role\": \"Project Manager\"\n", + "},\n", + "{\n", + "\"name\": \"Jane Smith\",\n", + "\"role\": \"Software Engineer\"\n", + "},\n", + "{\n", + "\"name\": \"Alice Johnson\",\n", + "\"role\": \"Data Scientist\"\n", + "}\n", + "],\n", + "\"task\": {\n", + "\"title\": \"Research existing multimodal document understanding systems\",\n", + "\"description\": \"Conduct a literature review and analyze current state-of-the-art systems in the field.\",\n", + "\"assigned_to\": \"John Doe\",\n", + "\"due_date\": [\"2022-10-15\"]\n", + "}\n", + "}\n", + "}\n", + "```\n", + "-------------\n", + "{\n", + " \"Project\": {\n", + " \"name\": \"Multimodal Document Understanding Project\",\n", + " \"description\": \"A project focused on developing a system for understanding documents using multiple modes of input such as text, images, and audio.\",\n", + " \"project_url\": \"https://example.com/multimodal-document-understanding\",\n", + " \"team_members\": [\n", + " {\n", + " \"name\": \"John Doe\",\n", + " \"role\": \"Project Manager\"\n", + " },\n", + " {\n", + " \"name\": \"Jane Smith\",\n", + " \"role\": \"Software Engineer\"\n", + " },\n", + " {\n", + " \"name\": \"Alice Johnson\",\n", + " \"role\": \"Data Scientist\"\n", + " }\n", + " ],\n", + " \"task\": {\n", + " \"title\": \"Research existing multimodal document understanding systems\",\n", + " \"description\": \"Conduct a literature review and analyze current state-of-the-art systems in the field.\",\n", + " \"assigned_to\": \"John Doe\",\n", + " \"due_date\": [\n", + " \"2022-10-15\"\n", + " ]\n", + " }\n", + " }\n", + "}\n", + "-------------\n", + "```\n", + "\n", + " \"Multimodal Document Understanding Project\" \n", + " \"This project aims to develop a system that can understand and analyze documents using multiple modes of input such as text, images, and audio.\" \n", + " \"https://www.multimodalproject.com\" \n", + " \n", + " \n", + " \"John Doe\" \n", + " \"Project Manager\" \n", + " \n", + " \n", + " \"Jane Smith\" \n", + " \"Lead Developer\" \n", + " \n", + " \n", + " \"Alice Johnson\" \n", + " \"Data Scientist\" \n", + " \n", + "\n", + " \n", + " \"Data Collection and Preprocessing\" \n", + " \"Collect and preprocess text, image, and audio data for training the multimodal document understanding system.\" \n", + " \"Alice Johnson\" \n", + " List[\"2022-10-15\"] \n", + "\n", + "\n", + "```\n", + "-------------\n", + "{'Project': {'name': 'Multimodal Document Understanding Project', 'description': 'This project aims to develop a system that can understand and analyze documents using multiple modes of input such as text, images, and audio.', 'project_url': 'https://www.multimodalproject.com', 'team_members': {'TeamMember': [{'name': 'John Doe', 'role': 'Project Manager'}, {'name': 'Jane Smith', 'role': 'Lead Developer'}, {'name': 'Alice Johnson', 'role': 'Data Scientist'}]}, 'task': {'title': 'Data Collection and Preprocessing', 'description': 'Collect and preprocess text, image, and audio data for training the multimodal document understanding system.', 'assigned_to': 'Alice Johnson', 'due_date': typing.List[ForwardRef('2022-10-15')]}}}\n", + "-------------\n", + "```\n", + "[Project]\n", + "name = \"Multimodal Document Understanding Project\"\n", + "description = \"A project focused on developing a system that can understand and analyze documents using multiple modes of input such as text, images, and audio.\"\n", + "project_url = \"https://www.multimodal-doc-understanding.com\"\n", + "team_members = [\n", + " [TeamMember]\n", + " name = \"John Doe\"\n", + " role = \"Project Manager\",\n", + " [TeamMember]\n", + " name = \"Jane Smith\"\n", + " role = \"Lead Developer\",\n", + " [TeamMember]\n", + " name = \"Alice Johnson\"\n", + " role = \"Data Scientist\"\n", + "]\n", + "task = [Task]\n", + "title = \"Develop Natural Language Processing Module\"\n", + "description = \"Create a module that can process and analyze text data from documents.\"\n", + "assigned_to = \"Jane Smith\"\n", + "due_date = \"2022-10-15\"\n", + "```\n", + "-------------\n" + ] + }, + { + "ename": "ParsingError", + "evalue": "ERROR: Unable to parse response into TOML format!", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mSyntaxError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:232\u001b[0m, in \u001b[0;36mTOML.parse_toml\u001b[0;34m(toml_string)\u001b[0m\n\u001b[1;32m 231\u001b[0m i \u001b[38;5;241m=\u001b[39m skip_whitespace(toml_string, i \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m--> 232\u001b[0m section, i \u001b[38;5;241m=\u001b[39m parse_section(toml_string, i)\n\u001b[1;32m 233\u001b[0m key \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(section\u001b[38;5;241m.\u001b[39mkeys())[\u001b[38;5;241m0\u001b[39m]\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:136\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_section\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 135\u001b[0m i \u001b[38;5;241m=\u001b[39m skip_whitespace(toml_string, i \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m--> 136\u001b[0m value, i \u001b[38;5;241m=\u001b[39m parse_value(toml_string, i)\n\u001b[1;32m 137\u001b[0m section[key\u001b[38;5;241m.\u001b[39mreplace(\u001b[38;5;124m'\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m'\u001b[39m)][subkey\u001b[38;5;241m.\u001b[39mreplace(\n\u001b[1;32m 138\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m'\u001b[39m)\u001b[38;5;241m.\u001b[39mstrip()] \u001b[38;5;241m=\u001b[39m value\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:147\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_value\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 146\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m toml_string[i] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m[\u001b[39m\u001b[38;5;124m'\u001b[39m:\n\u001b[0;32m--> 147\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m parse_array(toml_string, i)\n\u001b[1;32m 148\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m toml_string[i] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m{\u001b[39m\u001b[38;5;124m'\u001b[39m:\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:179\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_array\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 178\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m toml_string[i] \u001b[38;5;241m!=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m]\u001b[39m\u001b[38;5;124m'\u001b[39m:\n\u001b[0;32m--> 179\u001b[0m value, i \u001b[38;5;241m=\u001b[39m parse_value(toml_string, i)\n\u001b[1;32m 180\u001b[0m array \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m [value]\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:147\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_value\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 146\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m toml_string[i] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m[\u001b[39m\u001b[38;5;124m'\u001b[39m:\n\u001b[0;32m--> 147\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m parse_array(toml_string, i)\n\u001b[1;32m 148\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m toml_string[i] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m{\u001b[39m\u001b[38;5;124m'\u001b[39m:\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:179\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_array\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 178\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m toml_string[i] \u001b[38;5;241m!=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m]\u001b[39m\u001b[38;5;124m'\u001b[39m:\n\u001b[0;32m--> 179\u001b[0m value, i \u001b[38;5;241m=\u001b[39m parse_value(toml_string, i)\n\u001b[1;32m 180\u001b[0m array \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m [value]\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:151\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_value\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 150\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 151\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m parse_other(toml_string, i)\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:171\u001b[0m, in \u001b[0;36mTOML.parse_toml..parse_other\u001b[0;34m(toml_string, i)\u001b[0m\n\u001b[1;32m 170\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 171\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28meval\u001b[39m(val), i\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m:\n", + "\u001b[0;31mSyntaxError\u001b[0m: invalid syntax (, line 0)", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mParsingError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[15], line 37\u001b[0m\n\u001b[1;32m 34\u001b[0m llm_response \u001b[38;5;241m=\u001b[39m llm\u001b[38;5;241m.\u001b[39mrequest(prompt, temperature\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0.01\u001b[39m)\n\u001b[1;32m 35\u001b[0m log(llm_response)\n\u001b[0;32m---> 37\u001b[0m response \u001b[38;5;241m=\u001b[39m manager\u001b[38;5;241m.\u001b[39mparse(llm_response)\n\u001b[1;32m 38\u001b[0m log(response)\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/constrain.py:121\u001b[0m, in \u001b[0;36mConstrain.parse\u001b[0;34m(self, return_value)\u001b[0m\n\u001b[1;32m 118\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m return_value: \n\u001b[1;32m 119\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \n\u001b[0;32m--> 121\u001b[0m parsed_response \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mparse_helper(return_value)\n\u001b[1;32m 122\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Response(parsed_response)\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/constrain.py:147\u001b[0m, in \u001b[0;36mConstrain.parse_helper\u001b[0;34m(self, return_value)\u001b[0m\n\u001b[1;32m 145\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m JSON\u001b[38;5;241m.\u001b[39mparse(return_value)\n\u001b[1;32m 146\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mformat\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtoml\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m--> 147\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m TOML\u001b[38;5;241m.\u001b[39mparse(return_value)\n\u001b[1;32m 148\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mformat\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mxml\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 149\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m XML\u001b[38;5;241m.\u001b[39mparse(return_value)\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:248\u001b[0m, in \u001b[0;36mTOML.parse\u001b[0;34m(text)\u001b[0m\n\u001b[1;32m 246\u001b[0m \u001b[38;5;129m@staticmethod\u001b[39m\n\u001b[1;32m 247\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mparse\u001b[39m(text: \u001b[38;5;28mstr\u001b[39m):\n\u001b[0;32m--> 248\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m TOML\u001b[38;5;241m.\u001b[39mparse_toml(text)\n", + "File \u001b[0;32m/depot/euge/data/araviki/SyntaxShaper/grammarflow/grammars/toml.py:243\u001b[0m, in \u001b[0;36mTOML.parse_toml\u001b[0;34m(toml_string)\u001b[0m\n\u001b[1;32m 241\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m storage\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[0;32m--> 243\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ParsingError(\n\u001b[1;32m 244\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mERROR: Unable to parse response into TOML format!\u001b[39m\u001b[38;5;124m'\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mexc\u001b[39;00m\n", + "\u001b[0;31mParsingError\u001b[0m: ERROR: Unable to parse response into TOML format!" + ] + } + ], + "source": [ + "# You can add complex layers of grammars. You add even using Optional and Union types.\n", + "# For complex and nested grammars, JSON and XML are the best formats to use. \n", + "\n", + "class TeamMember(BaseModel):\n", + " name: str\n", + " role: str\n", + "\n", + "class Task(BaseModel):\n", + " title: str\n", + " description: str\n", + " assigned_to: str = Field(..., pattern=\"^(Akshath|Raghav|Ravikiran)$\")\n", + " due_date: List[str]\n", + "\n", + "class Project(BaseModel):\n", + " name: str\n", + " description: str\n", + " project_url: Optional[str] = None\n", + " team_members: List[TeamMember]\n", + " task: Task\n", + "\n", + "for serialization in ['json', 'xml']:\n", + " with Constrain(serialization) as manager:\n", + " system_context = \"\"\"You are a project manager and you are responsible for managing a project. You have to manage the project, it's grammars and other aspects. Ensure that you fill out all required fields.\"\"\"\n", + " user_message = \"\"\"Make me a project plan for a new project on multimodal document understanding projct.\"\"\"\n", + "\n", + " prompt = manager.format(llama_prompt, placeholders={'user_message': user_message,\n", + " 'system_context': system_context},\n", + " grammars=[{\n", + " 'description': 'This format elaborates on the project and its grammars.',\n", + " 'model': [Project]},\n", + " ]\n", + " )\n", + "\n", + " llm_response = llm.request(prompt, temperature=0.01)\n", + " log(llm_response)\n", + "\n", + " response = manager.parse(llm_response)\n", + " log(response)\n", + "\n", + " log(manager.inflation_rate())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# You can add multiple grammars to the same prompt. NOT RECOMMENDED.\n", + "# If you wish to do so, generally, TOML and JSON are the best formats to use.\n", + "\n", + "class EventIdea(BaseModel):\n", + " event_name: str\n", + " event_description: str\n", + " event_duration: str\n", + "\n", + "class BudgetPlan(BaseModel):\n", + " budget: float\n", + " items: List[str]\n", + " prices: List[int]\n", + " total_cost: int\n", + "\n", + "class EventSchedule(BaseModel):\n", + " event_name: str\n", + " event_time: float\n", + " event_duration: str\n", + "\n", + "prompt = \"I am hosting a birthday party for my girlfriend tomorrow. I want to buy a cake, balloons, some roses and ice cream. I have a budget of 500$. Can you create a sample event schedule and budget plan for me?.\"\n", + "\n", + "with Constrain('toml', 'multi_response') as manager:\n", + " prompt = manager.format(llama_prompt, \n", + " grammars=[\n", + " {\"task_description\": \"Brainstorming Event Ideas\", \"model\": EventIdea},\n", + " {\n", + " \"task_description\": \"Budget Planning And Activity Planning\",\n", + " \"model\": [BudgetPlan, EventSchedule],\n", + " },\n", + " ],\n", + " )\n", + "\n", + " llm_response = llm.request(prompt, temperature=0.01)\n", + " log(llm_response)\n", + "\n", + " response = manager.parse(llm_response)\n", + " log(response)\n", + "\n", + " log(manager.inflation_rate())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Grammars" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> \"GBNF (GGML BNF) is a format for defining formal grammars to constrain model outputs in llama.cpp. For example, you can use it to force the model to generate valid JSON, or speak only in emojis.\"\n", + "\n", + "Read more about it here: https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from grammarflow.grammars.gnbf import GNBF\n", + "from pydantic import BaseModel, Field\n", + "from typing import Optional, List" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "class TeamMember(BaseModel):\n", + " name: str\n", + " role: str\n", + "\n", + "class TaskUpdate(BaseModel):\n", + " update_time: float\n", + " comment: Optional[str] = None\n", + " status: bool\n", + "\n", + "class Task(BaseModel):\n", + " title: str\n", + " description: str\n", + " assigned_to: str = Field(..., pattern='(\"Akshath\"|\"Raghav\"|\"Ravikiran\")')\n", + " due_date: List[str]\n", + " updates: List[TaskUpdate]\n", + "\n", + "class Project(BaseModel):\n", + " name: str\n", + " description: str\n", + " project_url: Optional[str] = Field(None, pattern='\"https://\"[0-9a-fA-F]*')\n", + " team_members: List[TeamMember] \n", + " task: Task" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "grammar = GNBF(Project).generate_grammar()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "root ::= ws Project\n", + "Project ::= nl \"{\" \"\\\"Project\\\":\" ws \"{\" ws \"\\\"name\\\":\" ws string \",\" nl \"\\\"description\\\":\" ws string \",\" nl \"\\\"project-url\\\":\" ws project-url \",\" nl \"\\\"team-members\\\":\" ws TeamMember \",\" nl \"\\\"task\\\":\" ws Task \"}\" ws \"}\"\n", + "project-url ::= \"https://\"[0-9a-fA-F]\n", + "assigned-to ::= (\"Akshath\"|\"Raghav\"|\"Ravikiran\")\n", + "ws ::= [ \\t\\n]\n", + "nl ::= [\\n]\n", + "string ::= \"\\\"\" (\n", + " [^\"\\\\] |\n", + " \"\\\\\" ([\"\\\\/bfnrt] | \"u\" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])\n", + " )* \"\\\"\"\n", + "TeamMember ::= nl \"{\" ws \"\\\"name\\\":\" ws string \",\" nl \"\\\"role\\\":\" ws string \"}\"\n", + "number ::= (\"-\"? ([0-9] | [1-9] [0-9]*)) (\".\" [0-9]+)? ([eE] [-+]? [0-9]+)?\n", + "boolean ::= (\"True\" | \"False\")\n", + "TaskUpdate ::= nl \"{\" ws \"\\\"update-time\\\":\" ws number \",\" nl \"\\\"comment\\\":\" ws string \",\" nl \"\\\"status\\\":\" ws boolean \"}\"\n", + "array ::= \"[\" ws (\n", + " due-date-value\n", + " (\",\" ws due-date-value)*\n", + " )? \"]\" ws\n", + "due-date-value ::= string\n", + "Task ::= nl \"{\" ws \"\\\"title\\\":\" ws string \",\" nl \"\\\"description\\\":\" ws string \",\" nl \"\\\"assigned-to\\\":\" ws assigned-to \",\" nl \"\\\"due-date\\\":\" ws array \",\" nl \"\\\"updates\\\":\" ws TaskUpdate \"}\"\n" + ] + } + ], + "source": [ + "print(grammar)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "from_string grammar:\n", + "root ::= ws Project \n", + "ws ::= [ ] \n", + "Project ::= nl [{] [\"] [P] [r] [o] [j] [e] [c] [t] [\"] [:] ws [{] ws [\"] [n] [a] [m] [e] [\"] [:] ws string [,] nl [\"] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [\"] [:] ws string [,] nl [\"] [p] [r] [o] [j] [e] [c] [t] [-] [u] [r] [l] [\"] [:] ws project-url [,] nl [\"] [t] [e] [a] [m] [-] [m] [e] [m] [b] [e] [r] [s] [\"] [:] ws TeamMember [,] nl [\"] [t] [a] [s] [k] [\"] [:] ws Task [}] ws [}] \n", + "nl ::= [] \n", + "string ::= [\"] string_12 [\"] \n", + "project-url ::= [h] [t] [t] [p] [s] [:] [/] [/] [0-9a-fA-F] \n", + "TeamMember ::= nl [{] ws [\"] [n] [a] [m] [e] [\"] [:] ws string [,] nl [\"] [r] [o] [l] [e] [\"] [:] ws string [}] \n", + "Task ::= nl [{] ws [\"] [t] [i] [t] [l] [e] [\"] [:] ws string [,] nl [\"] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [\"] [:] ws string [,] nl [\"] [a] [s] [s] [i] [g] [n] [e] [d] [-] [t] [o] [\"] [:] ws assigned-to [,] nl [\"] [d] [u] [e] [-] [d] [a] [t] [e] [\"] [:] ws array [,] nl [\"] [u] [p] [d] [a] [t] [e] [s] [\"] [:] ws TaskUpdate [}] \n", + "assigned-to ::= assigned-to_9 \n", + "assigned-to_9 ::= [A] [k] [s] [h] [a] [t] [h] | [R] [a] [g] [h] [a] [v] | [R] [a] [v] [i] [k] [i] [r] [a] [n] \n", + "string_10 ::= [^\"\\] | [\\] string_11 \n", + "string_11 ::= [\"\\/bfnrt] | [u] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] \n", + "string_12 ::= string_10 string_12 | \n", + "number ::= number_14 number_20 number_24 \n", + "number_14 ::= number_15 number_16 \n", + "number_15 ::= [-] | \n", + "number_16 ::= [0-9] | [1-9] number_17 \n", + "number_17 ::= [0-9] number_17 | \n", + "number_18 ::= [.] number_19 \n", + "number_19 ::= [0-9] number_19 | [0-9] \n", + "number_20 ::= number_18 | \n", + "number_21 ::= [eE] number_22 number_23 \n", + "number_22 ::= [-+] | \n", + "number_23 ::= [0-9] number_23 | [0-9] \n", + "number_24 ::= number_21 | \n", + "boolean ::= boolean_26 \n", + "boolean_26 ::= [T] [r] [u] [e] | [F] [a] [l] [s] [e] \n", + "TaskUpdate ::= nl [{] ws [\"] [u] [p] [d] [a] [t] [e] [-] [t] [i] [m] [e] [\"] [:] ws number [,] nl [\"] [c] [o] [m] [m] [e] [n] [t] [\"] [:] ws string [,] nl [\"] [s] [t] [a] [t] [u] [s] [\"] [:] ws boolean [}] \n", + "array ::= [[] ws array_33 []] ws \n", + "array_29 ::= due-date-value array_32 \n", + "due-date-value ::= string \n", + "array_31 ::= [,] ws due-date-value \n", + "array_32 ::= array_31 array_32 | \n", + "array_33 ::= array_29 | \n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Using llama.cpp, we can verify if our grammar string is accepted.\n", + "# If successful, no error is thrown. Unfortunately, llama-cpp-python prints out the syntax tree to stdout. \n", + "GNBF.verify_grammar(grammar)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "from_string grammar:\n", + "root ::= ws Project \n", + "ws ::= [ ] \n", + "Project ::= [<] [P] [r] [o] [j] [e] [c] [t] [>] ws [<] [n] [a] [m] [e] [>] ws string ws [<] [/] [n] [a] [m] [e] [>] ws [<] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [>] ws string ws [<] [/] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [>] ws [<] [p] [r] [o] [j] [e] [c] [t] [-] [u] [r] [l] [>] ws [h] [t] [t] [p] [s] [:] [/] [/] [.] [*] ws [<] [/] [p] [r] [o] [j] [e] [c] [t] [-] [u] [r] [l] [>] ws [<] [t] [e] [a] [m] [-] [m] [e] [m] [b] [e] [r] [s] [>] ws [^] [(] [A] [k] [s] [h] [a] [t] [h] [|] [R] [a] [g] [h] [a] [v] [|] [R] [a] [v] [i] [k] [i] [r] [a] [n] [)] [$] ws [<] [/] [t] [e] [a] [m] [-] [m] [e] [m] [b] [e] [r] [s] [>] ws [<] [g] [r] [a] [m] [m] [a] [r] [s] [>] ws Task ws [<] [/] [g] [r] [a] [m] [m] [a] [r] [s] [>] ws [<] [/] [P] [r] [o] [j] [e] [c] [t] [>] \n", + "string ::= [\"] string_20 [\"] \n", + "Task ::= [<] [t] [i] [t] [l] [e] [>] ws string ws [<] [/] [t] [i] [t] [l] [e] [>] ws [<] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [>] ws string ws [<] [/] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [>] ws [<] [a] [s] [s] [i] [g] [n] [e] [d] [-] [t] [o] [>] ws [^] [(] [A] [k] [s] [h] [a] [t] [h] [|] [R] [a] [g] [h] [a] [v] [|] [R] [a] [v] [i] [k] [i] [r] [a] [n] [)] [$] ws [<] [/] [a] [s] [s] [i] [g] [n] [e] [d] [-] [t] [o] [>] ws [<] [d] [u] [e] [-] [d] [a] [t] [e] [>] ws array ws [<] [/] [d] [u] [e] [-] [d] [a] [t] [e] [>] ws [<] [u] [p] [d] [a] [t] [e] [s] [>] ws TaskUpdate ws [<] [/] [u] [p] [d] [a] [t] [e] [s] [>] \n", + "nl ::= [] \n", + "number ::= number_7 number_13 number_17 \n", + "number_7 ::= number_8 number_9 \n", + "number_8 ::= [-] | \n", + "number_9 ::= [0-9] | [1-9] number_10 \n", + "number_10 ::= [0-9] number_10 | \n", + "number_11 ::= [.] number_12 \n", + "number_12 ::= [0-9] number_12 | [0-9] \n", + "number_13 ::= number_11 | \n", + "number_14 ::= [eE] number_15 number_16 \n", + "number_15 ::= [-+] | \n", + "number_16 ::= [0-9] number_16 | [0-9] \n", + "number_17 ::= number_14 | \n", + "string_18 ::= [^\"\\] | [\\] string_19 \n", + "string_19 ::= [\"\\/bfnrt] | [u] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] \n", + "string_20 ::= string_18 string_20 | \n", + "boolean ::= boolean_22 \n", + "boolean_22 ::= [T] [r] [u] [e] | [F] [a] [l] [s] [e] \n", + "TaskUpdate ::= [<] [u] [p] [d] [a] [t] [e] [-] [t] [i] [m] [e] [>] ws number ws [<] [/] [u] [p] [d] [a] [t] [e] [-] [t] [i] [m] [e] [>] ws [<] [c] [o] [m] [m] [e] [n] [t] [>] ws string ws [<] [/] [c] [o] [m] [m] [e] [n] [t] [>] ws [<] [s] [t] [a] [t] [u] [s] [>] ws boolean ws [<] [/] [s] [t] [a] [t] [u] [s] [>] \n", + "array ::= [[] ws array_29 []] ws \n", + "array_25 ::= due-date-value array_28 \n", + "due-date-value ::= string \n", + "array_27 ::= [,] ws due-date-value \n", + "array_28 ::= array_27 array_28 | \n", + "array_29 ::= array_25 | \n", + "\n", + "from_string grammar:\n", + "root ::= ws Project \n", + "ws ::= [ ] \n", + "Project ::= [[] [P] [r] [o] [j] [e] [c] [t] []] nl [n] [a] [m] [e] ws [=] ws string nl [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] ws [=] ws string nl [h] [t] [t] [p] [s] [:] [/] [/] [.] [*] nl [^] [(] [A] [k] [s] [h] [a] [t] [h] [|] [R] [a] [g] [h] [a] [v] [|] [R] [a] [v] [i] [k] [i] [r] [a] [n] [)] [$] nl Task \n", + "nl ::= [] \n", + "string ::= [\"] string_20 [\"] \n", + "Task ::= [t] [i] [t] [l] [e] ws [=] ws string nl [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] ws [=] ws string nl [^] [(] [A] [k] [s] [h] [a] [t] [h] [|] [R] [a] [g] [h] [a] [v] [|] [R] [a] [v] [i] [k] [i] [r] [a] [n] [)] [$] nl [d] [u] [e] [-] [d] [a] [t] [e] ws [=] ws array nl TaskUpdate \n", + "number ::= number_7 number_13 number_17 \n", + "number_7 ::= number_8 number_9 \n", + "number_8 ::= [-] | \n", + "number_9 ::= [0-9] | [1-9] number_10 \n", + "number_10 ::= [0-9] number_10 | \n", + "number_11 ::= [.] number_12 \n", + "number_12 ::= [0-9] number_12 | [0-9] \n", + "number_13 ::= number_11 | \n", + "number_14 ::= [eE] number_15 number_16 \n", + "number_15 ::= [-+] | \n", + "number_16 ::= [0-9] number_16 | [0-9] \n", + "number_17 ::= number_14 | \n", + "string_18 ::= [^\"\\] | [\\] string_19 \n", + "string_19 ::= [\"\\/bfnrt] | [u] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] \n", + "string_20 ::= string_18 string_20 | \n", + "boolean ::= boolean_22 \n", + "boolean_22 ::= [T] [r] [u] [e] | [F] [a] [l] [s] [e] \n", + "TaskUpdate ::= [u] [p] [d] [a] [t] [e] [-] [t] [i] [m] [e] ws [=] ws number nl [c] [o] [m] [m] [e] [n] [t] ws [=] ws string nl [s] [t] [a] [t] [u] [s] ws [=] ws boolean \n", + "array ::= [[] ws array_29 []] ws \n", + "array_25 ::= due-date-value array_28 \n", + "due-date-value ::= string \n", + "array_27 ::= [,] ws due-date-value \n", + "array_28 ::= array_27 array_28 | \n", + "array_29 ::= array_25 | \n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grammar = GNBF(Project).generate_grammar('xml')\n", + "GNBF.verify_grammar(grammar)\n", + "\n", + "grammar = GNBF(Project).generate_grammar('toml')\n", + "GNBF.verify_grammar(grammar) " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..f041198 --- /dev/null +++ b/pylintrc @@ -0,0 +1,407 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=R, + trailing-whitespace, + unspecified-encoding, + unused-argument, + inconsistent-quotes, + line-too-long, + broad-exception-caught, + eval-used, + missing-final-newline, + missing-module-docstring, + abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat, + import-error, + import-self, + import-star-module-level, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=12 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=2 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/samples/bert_finetuning/annotator.ipynb b/samples/bert_finetuning/annotator.ipynb index 48a65f0..10ef6b7 100644 --- a/samples/bert_finetuning/annotator.ipynb +++ b/samples/bert_finetuning/annotator.ipynb @@ -67,10 +67,6 @@ " api_key=os.environ[\"OPENAI_API_KEY\"],\n", " )\n", "\n", - " def invoke(self, config: dict):\n", - " with PromptContextManager(config) as filled_prompt:\n", - " return self.request(filled_prompt, temperature=0.01)\n", - "\n", " def __call__(self, prompt, temperature=0.2, context=None):\n", " response = self.client.chat.completions.create(\n", " model=\"gpt-3.5-turbo\",\n", @@ -236,17 +232,13 @@ "metadata": {}, "outputs": [], "source": [ - "with Constrain(prompt) as manager:\n", - " manager.set_config(format=\"xml\")\n", - "\n", - " manager.format_prompt(\n", + "with Constrain('xml') as manager:\n", + " prompt = manager.format(prompt, \n", " placeholders={\"abstract\": abstract, \"sample\": XML.format(SampleAnnotations)},\n", - " grammars=[\n", - " {\"model\": [Annotations]},\n", - " ],\n", + " grammars=[{\"model\": [Annotations]}]\n", " )\n", "\n", - " llm_response = llm(manager.prompt)\n", + " llm_response = llm(prompt)\n", " response = manager.parse(llm_response)" ] }, diff --git a/samples/demo.ipynb b/samples/demo.ipynb deleted file mode 100644 index 4ed4116..0000000 --- a/samples/demo.ipynb +++ /dev/null @@ -1,1304 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from openai import OpenAI\n", - "from pydantic import BaseModel, Field\n", - "from typing import Any, List, Tuple, Type, Optional, Union\n", - "import os\n", - "import json" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from dotenv import load_dotenv, find_dotenv\n", - "\n", - "load_dotenv(find_dotenv())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Helpers!" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "class LLM:\n", - " def __init__(self):\n", - " self.client = OpenAI(\n", - " api_key=os.environ[\"OPENAI_API_KEY\"],\n", - " )\n", - "\n", - " def invoke(self, config: dict):\n", - " with PromptContextManager(config) as filled_prompt:\n", - " return self.request(filled_prompt, temperature=0.01)\n", - "\n", - " def request(self, prompt, temperature=0.2, context=None):\n", - " response = self.client.chat.completions.create(\n", - " model=\"gpt-3.5-turbo\",\n", - " messages=[{\"role\": \"user\", \"content\": prompt}],\n", - " temperature=temperature,\n", - " )\n", - " return response.choices[0].message.content\n", - "\n", - "\n", - "llm = LLM()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "def log(msg):\n", - " print(msg)\n", - " print(\"-------------\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Making Prompts!" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "from grammarflow.prompt.builder import PromptBuilder # Prompt builder" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can using PromptBuilder to make your own prompt templates. Otherwise, you can just pass in your prompt as a string and `constrain` does the needful. \n", - "\n", - "If using PromptBuilder, you will add sections within your prompt. Each section takes these attributes: \n", - "- text: str, Fixed text; Can have placeholders, but needs to be specified with `placeholder` attribute.\n", - "- placeholder: str, Exact `placeholder` identifiers in `text`.\n", - "- define_grammar: True/False, Can be used only once in the template. Defines the section where grammar will be explained.\n", - "- add_few_shot_examples: True/False, Adds a filled-up Pydantic model to the prompt aligning with the grammar format." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# Sample Llama Prompt\n", - "\n", - "llama_prompt = PromptBuilder()\n", - "llama_prompt.add_section(\n", - " text=\"[INST] <>\\n{system_context}\\n<>\",\n", - " placeholders=\"system_context\",\n", - " define_grammar=True,\n", - ")\n", - "llama_prompt.add_section(\n", - " text=\"{user_message}[/INST]\",\n", - " placeholders=\"user_message\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using Constrain:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "from grammarflow.constrain import Constrain # Main class" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "1. Begin by creating a `Constrain` object with a previously defined `prompt_config`. You can instead, just pass in a string which contains your prompt. \n", - "\n", - "2. Use `set_config` method on the `Constrain` instance to specify the output format and how the responses should be structured. In this case:\n", - " - `format`: Specifies the output format, here set to 'json'. Can be 'XML' or 'TOML' \n", - " - `return_sequence`: Determines how responses are returned. Can be 'single_response' or 'multiple_response'. \n", - "\n", - "3. The `format_prompt` method is used to assemble the final prompt using placeholders and task-specific configurations:\n", - " - `placeholders`: Needs to be a dict of {'placeholder': 'string'}. Required, if your prompt template contains placeholders. \n", - " - `grammars`: A list of grammars. Each task includes:\n", - " - `description`: A brief explanation of what the task entails or aims to achieve. Optional.\n", - " - `model`: Needs to be an empty pydantic model. \n", - "\n", - "4. Upon uisng `format_prompt`, `manager` holds the final prompt. You can get it by calling `manager.prompt`\n", - "\n", - "5. `manager.parse(llm_response)` takes in the response string and outputs the parsed object. It will return `Response` object, which is a simple wrapper for easy accessing of nested fields. \n", - "\n", - "5. You can view the inflation rate of tokens using `manager.inflation_rate()`" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "user_message = \"\"\n", - "system_context = \"\"\n", - "\n", - "\n", - "class Model(BaseModel):\n", - " model_name: str\n", - "\n", - "\n", - "with Constrain(llama_prompt) as manager:\n", - " manager.set_config(\n", - " format='json',\n", - " return_sequence='single_response'\n", - " )\n", - "\n", - " manager.format_prompt(placeholders={'user_message': user_message,\n", - " 'system_context': system_context},\n", - " grammars=[{\n", - " 'model': [Model]},\n", - " ]\n", - " )\n", - "\n", - " prompt = manager.prompt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Examples! " - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "class Annotations(BaseModel): \n", - " materials: List[str] = Field(..., description=\"ADD SOMETHING\")\n", - " conditions: List[str]= Field(..., description=\"ADD SOMETHING\")\n", - " parameters: List[str]= Field(..., description=\"ADD SOMETHING\")\n", - " processes: List[str]= Field(..., description=\"ADD SOMETHING\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "prompt = PromptBuilder() \n", - "prompt.add_section(\n", - " text=\"\"\"\n", - "Your role is that of a DATA ANNOTATOR for research paper abstracts. You are expected to identify the materials used, different processes involved (such as types of chromatography, purification, methods, preparation, study, etc), and conditions (temperature units, quantity units, mathematical units, percentages, coefficients, any numbers) and any parameters- like factor names, rate, yield, etc mentioned in the abstract. \n", - "\n", - "I want you to look at the abstract given below and return all the key phrases you find. \n", - " \"\"\"\n", - ") \n", - "prompt.add_section(\n", - " define_grammar = True\n", - ")\n", - "prompt.add_section(\n", - " text=\"\"\"\n", - "Here is an example: \n", - "\n", - "Abstract: Of the three particle sizes studied (10µm, 20µm, 50µm) only 10µm silica resin was able to produce purified API at the yield (>96%) and productivity (> 1kg/kg-resin/day) necessitated by the project. The second case study uses DoE studies to identify critical process parameters of column load, mobile phase solvent ratio and basic modifier level for a low-resolution, preparative, chiral separation.\n", - "Annotations: \n", - "```xml\n", - "['silica resin'] \n", - "['10µm, 20µm, 50µm', 'only 10µm silica resin, ' yield (>96%)', 'productivity (> 1kg/kg-resin/day)',] \n", - "[ ' column load', 'mobile phase solvent ratio']\n", - "[ 'chiral separation']\n", - "```\n", - "\n", - "Every str object within the list you return for each of the tags must contain at least 2 words and not exceed 8 words. Remember that your role is to automate the data annotation process for a chemistry based project. \n", - "\n", - "Begin!\n", - "\"\"\"\n", - ")\n", - "prompt.add_section(\n", - " text=\"Abstract: {abstract}\\nAnnotations:\", \n", - " placeholders=[\"abstract\"]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "abstract = \"\"\" The simultaneous determination of multi-mycotoxins in food commodities are highly desirable due to their potential toxic effects and mass consumption of foods. Herein, liquid chromatography-quadrupole exactive orbitrap mass spectrometry was proposed to analyze multi-mycotoxins in commercial vegetable oils. Specifically, the method featured a successive liquid–liquid extraction process, in which the complementary solvents consisted of acetonitrile and water were optimized. Resultantly, matrix effects were reduced greatly. External calibration approach revealed good quantification property for each analyte. Under optimal conditions, the recovery ranging from 80.8% to 109.7%, relative standard deviation less than 11.7%, and good limit of quantification (0.35 to 45.4ng/g) were achieved. The high accuracy of proposed method was also validated. The detection of 20 commercial vegetable oils revealed that aflatoxins B1 and B2, zearalenone were observed in 10 real samples. The as-developed method is simple and low-cost, which merits the wide applications for scanning mycotoxins in oil matrices.\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "with Constrain(prompt) as manager:\n", - " manager.set_config(\n", - " format='xml',\n", - " return_sequence='single_response'\n", - " )\n", - "\n", - " manager.format_prompt(placeholders={\n", - " \"abstract\": abstract}, \n", - " grammars=[{\n", - " 'model': [Annotations]},\n", - " ]\n", - " )\n", - "\n", - " prompt = manager.prompt\n", - " llm_response = llm.request(prompt)\n", - " response = manager.parse(llm_response)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"Annotations\": {\n", - " \"materials\": \"['commercial vegetable oils', 'acetonitrile', 'water']\",\n", - " \"conditions\": \"['liquid chromatography-quadrupole exactive orbitrap mass spectrometry', 'successive liquid\\u2013liquid extraction process', 'acetonitrile and water were optimized', 'under optimal conditions', 'good limit of quantification (0.35 to 45.4ng/g)']\",\n", - " \"parameters\": \"['recovery ranging', 'relative standard deviation', 'limit of quantification']\",\n", - " \"processes\": \"['liquid chromatography-quadrupole exactive orbitrap mass spectrometry', 'liquid\\u2013liquid extraction process', 'external calibration approach']\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "print(response)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"['liquid chromatography-quadrupole exactive orbitrap mass spectrometry', 'successive liquid–liquid extraction process', 'acetonitrile and water were optimized', 'under optimal conditions', 'good limit of quantification (0.35 to 45.4ng/g)']\"" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "response.Annotations.conditions" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "ename": "ValueError", - "evalue": "Stop here", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[14], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mStop here\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", - "\u001b[0;31mValueError\u001b[0m: Stop here" - ] - } - ], - "source": [ - "raise ValueError(\"Stop here\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def check_previous_interaction(id_): return id_ > 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class Step(BaseModel):\n", - " thought: str = Field(..., description=\"This should concisely explain what you want to know for your goal.\")\n", - " action: str = Field(..., description=\"Your options: \\\n", - "'load_md_file' (Provide the name of the file you want to load. Eg: 'README.md') | 'get_link_from_filename' (Provide the name (can be incomplete) of the file, and get it's link).\")\n", - " action_input: str = Field(..., description=\"The input for the action you want to take. Eg: 'README.md' | 'readme'\")\n", - " \n", - "prompt = PromptBuilder() \n", - "prompt.add_section(\n", - " text=\"Your role is that of a {role}. In this ongoing conversation, your goal is to {goal}.\\nYour final result should contain {deliverables}.\", \n", - " placeholders=[\"role\", \"goal\", \"deliverables\"]\n", - ")\n", - "prompt.add_section(\n", - " define_grammar=True\n", - ") \n", - "prompt.add_section(\n", - " text=\"\\nIn our previous interaction, you wanted to {thought} using {action}. You observed: {observation}.\",\n", - " placeholders=[\"thought\", \"action\", \"observation\"], \n", - " enable_on=check_previous_interaction\n", - ")\n", - "prompt.add_section(\n", - " text=\"Create the next Step in the conversation. Think through your reasoning and the action you want to take. Ensure that you are progressing towards your goal.\",\n", - " enable_on=check_previous_interaction\n", - ")\n", - "prompt.add_section(\n", - " text=\"Below is the history of the conversation so far.\\n{hsitory}\\n\",\n", - " placeholders=[\"history\"],\n", - " enable_on=check_previous_interaction\n", - ") " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "role = \"software developer trying to reproduce a codebase\"\n", - "goal = \"create a roadmap to set up the environment a github repository\"\n", - "deliverables = \"the links to the files needed in each step of your roadmap, and the code for each step\"\n", - "thought = None \n", - "action = None\n", - "observation = None\n", - "history = None\n", - "\n", - "id_ = 0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Your role is that of a software developer trying to reproduce a codebase. In this ongoing conversation, your goal is to create a roadmap to set up the environment a github repository.\n", - "Your final result should contain the links to the files needed in each step of your roadmap, and the code for each step.\n", - "\n", - "Here is the XML output format you are expected to return your response in.\n", - "\n", - "Your thinking state\n", - "```\n", - "\n", - " #string# # This should concisely explain what you want to know for your goal.\n", - " #string# # Your options: 'load_md_file' (Provide the name of the file you want to load. Eg: 'README.md') | 'get_link_from_filename' (Provide the name (can be incomplete) of the file, and get it's link).\n", - " #string# # The input for the action you want to take. Eg: 'README.md' | 'readme'\n", - "\n", - "\n", - "```\n", - "\n", - "RETURN ONLY ONE OF . DO NOT FORGET TO COVER YOUR OUTPUTS WITH '```'.\n" - ] - } - ], - "source": [ - "with Constrain(prompt) as manager: \n", - " manager.set_config(\n", - " format='xml'\n", - " ) \n", - " manager.format_prompt(\n", - " placeholders={ \n", - " \"role\": role, \n", - " \"goal\": goal, \n", - " \"deliverables\": deliverables,\n", - " \"thought\": thought, \n", - " \"action\": action,\n", - " \"observation\": observation,\n", - " \"history\": history\n", - " }, \n", - " grammars=[{\n", - " 'description': 'Your thinking state', \n", - " 'model': Step\n", - " }], \n", - " enable_on={\n", - " 'id_':id_ \n", - " }\n", - " ) \n", - " \n", - " print(manager.prompt)\n", - " # resp = llm(manager.prompt, temperature=0.01)\n", - "\n", - " # thought = response.Step.thought \n", - " # action = response.Step.action\n", - " # action_input = response.Step.action_input\n", - "\n", - " # if action == \"load_md_file\": \n", - " # git.find_files(action_input)\n", - " # elif action == \"get_link_from_filename\":\n", - " # git.find_files(action_input)\n", - " # git.get_file_url(action_input)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "ename": "Exception", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[27], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m()\n", - "\u001b[0;31mException\u001b[0m: " - ] - } - ], - "source": [ - "raise Exception()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "```\n", - "\n", - "fib\n", - "Function to generate the Fibonacci sequence\n", - "numpy\n", - "123456789\n", - "true\n", - "\n", - "def fib(n):\n", - " import numpy as np\n", - " a, b = 0, 1\n", - " result = []\n", - " for _ in range(n):\n", - " result.append(a)\n", - " a, b = b, a + b\n", - " return np.array(result)\n", - "\n", - "\n", - "```\n", - "-------------\n", - "{\n", - " \"FunctionModel\": {\n", - " \"function_name\": \"fib\",\n", - " \"docstring\": \"Function to generate the Fibonacci sequence\",\n", - " \"depedencies\": \"numpy\",\n", - " \"uuid\": 123456789,\n", - " \"is_python\": true,\n", - " \"code\": \"def fib(n): import numpy as np a, b = 0, 1 result = [] for _ in range(n): result.append(a) a, b = b, a + b return np.array(result)\"\n", - " }\n", - "}\n", - "-------------\n", - "{'before': 27, 'after': 146, 'factor': '4.4x'}\n", - "-------------\n" - ] - } - ], - "source": [ - "# Here's a simple example of asking an LLM to make code.\n", - "# This can be used within coding assistants which requires extra metadata.\n", - "\n", - "\n", - "class FunctionModel(BaseModel):\n", - " function_name: str\n", - " docstring: str\n", - " depedencies: List[str]\n", - " uuid: Union[float, int]\n", - " is_python: bool\n", - " code: str\n", - "\n", - "\n", - "input_str = \"I want to create a function that returns the fibonacci sequence. The function should be called 'fib'. The function can use numpy.\"\n", - "\n", - "with Constrain(input_str) as manager:\n", - " manager.set_config(format=\"xml\", return_sequence=\"single_response\")\n", - "\n", - " manager.format_prompt(\n", - " grammars=[{\"description\": \"No Code Generation\", \"model\": FunctionModel}]\n", - " )\n", - "\n", - " prompt = manager.prompt\n", - "\n", - " llm_response = llm.request(prompt, temperature=0.01)\n", - " log(llm_response)\n", - "\n", - " response = manager.parse(llm_response)\n", - " log(response)\n", - "\n", - " log(manager.inflation_rate())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n" - ] - } - ], - "source": [ - "# The response will be of `Response` type, which can be used to extract the data. If adding the parsed response to this object fails, it will return the dict itself.\n", - "print(response.FunctionModel.is_python)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "```\n", - "\n", - " fib \n", - " This function returns the Fibonacci sequence up to the input number. \n", - " ['numpy'] \n", - " 987654321 \n", - " True \n", - " def fib(n):\n", - " a, b = 0, 1\n", - " result = []\n", - " while a < n:\n", - " result.append(a)\n", - " a, b = b, a + b\n", - " return result \n", - "\n", - "```\n", - "-------------\n", - "{\n", - " \"FunctionModel\": {\n", - " \"function_name\": \"fib\",\n", - " \"docstring\": \"This function returns the Fibonacci sequence up to the input number.\",\n", - " \"depedencies\": \"['numpy']\",\n", - " \"uuid\": 987654321,\n", - " \"is_python\": true,\n", - " \"code\": \"def fib(n): a, b = 0, 1 result = [] while a < n: result.append(a) a, b = b, a + b return result\"\n", - " }\n", - "}\n", - "-------------\n", - "{'before': 27, 'after': 252, 'factor': '8.3x'}\n", - "-------------\n" - ] - } - ], - "source": [ - "# Add some examples too!\n", - "\n", - "Sum_Function_Model = FunctionModel(\n", - " function_name=\"sum\",\n", - " docstring=\"This function returns the sum of the input list.\",\n", - " depedencies=[\"numpy\"],\n", - " uuid=123456789,\n", - " is_python=True,\n", - " code=\"def sum(a, b):\\n\\treturn a + b\"\n", - ")\n", - "\n", - "\n", - "with Constrain(input_str) as manager:\n", - " manager.set_config(\n", - " format='xml',\n", - " return_sequence='single_response'\n", - " )\n", - "\n", - " manager.format_prompt(\n", - " grammars=[\n", - " {\n", - " 'description': 'No Code Generation',\n", - " 'model': FunctionModel\n", - " }\n", - " ],\n", - " examples=[\n", - " {\n", - " 'query': \"Create a summation function in Python\",\n", - " 'model': Sum_Function_Model\n", - " }\n", - " ]\n", - " )\n", - "\n", - " prompt = manager.prompt\n", - "\n", - " llm_response = llm.request(prompt, temperature=0.01)\n", - " log(llm_response)\n", - "\n", - " response = manager.parse(llm_response)\n", - " log(response)\n", - "\n", - " log(manager.inflation_rate())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "```json\n", - "{\n", - "\"ThoughtState\": {\n", - "\"thought\": \"Vladimir Putin is the President of Russia.\",\n", - "\"goal\": \"To provide information about Vladimir Putin.\",\n", - "\"tool\": \"Web_Search\",\n", - "\"action\": \"Read\",\n", - "\"action_input\": \"Vladimir Putin\",\n", - "\"thought_id\": \"12345\"\n", - " }\n", - "}\n", - "```\n", - "-------------\n", - "{\n", - " \"ThoughtState\": {\n", - " \"thought\": \"Vladimir Putin is the President of Russia.\",\n", - " \"goal\": \"To provide information about Vladimir Putin.\",\n", - " \"tool\": \"Web_Search\",\n", - " \"action\": \"Read\",\n", - " \"action_input\": \"Vladimir Putin\",\n", - " \"thought_id\": \"12345\"\n", - " }\n", - "}\n", - "-------------\n", - "{'before': 115, 'after': 277, 'factor': '1.4x'}\n", - "-------------\n" - ] - } - ], - "source": [ - "# Sample ReAct Model with Llama Prompt\n", - "# You can add descriptions within the grammar model to provide it's context and options. This is how we use the LLM in https://github.com/e-lab/Forestry_Student/.\n", - "\n", - "class ThoughtState(BaseModel):\n", - " thought: str\n", - " goal: str\n", - " tool: str = Field(...,\n", - " description=\"Choose one of ['Web_QA', 'Web_Search', 'Web_Scraping', 'Web_Automation', 'Web_Research']\")\n", - " action: str = Field(...,\n", - " description=\"Choose one of ['Create', 'Update', 'Delete', 'Read']\")\n", - " action_input: str = Field(..., description=\"The input data for the action\")\n", - " thought_id: Optional[str] = Field(\n", - " None, description=\"The unique identifier for the thought\")\n", - "\n", - "\n", - "system_context = \"\"\"Your goal is to think and plan out how to solve questions using agent tools provided to you. Think about all aspects of your thought process.\"\"\"\n", - "user_message = \"\"\"Who is Vladmir Putin?\"\"\"\n", - "\n", - "with Constrain(llama_prompt) as manager:\n", - " manager.set_config(\n", - " format='json',\n", - " return_sequence='single_response'\n", - " )\n", - "\n", - " manager.format_prompt(placeholders={\n", - " 'user_message': user_message,\n", - " 'system_context': system_context\n", - " },\n", - " grammars=[{\n", - " 'description': 'This format describes your current thinking state',\n", - " 'model': [ThoughtState]},\n", - " ]\n", - " )\n", - "\n", - " prompt = manager.prompt\n", - "\n", - " llm_response = llm.request(prompt, temperature=0.01)\n", - " log(llm_response)\n", - "\n", - " response = manager.parse(llm_response)\n", - " log(response)\n", - "\n", - " log(manager.inflation_rate())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Web_Search\n" - ] - } - ], - "source": [ - "# You can then access the response from the `response` object\n", - "print(response.ThoughtState.tool)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "``` \n", - "\n", - " Multimodal Document Understanding Project \n", - " This project aims to develop a system that can understand and analyze multimodal documents, including text, images, and videos. \n", - " www.multimodalproject.com \n", - "\n", - " \n", - " John Doe \n", - " Project Manager \n", - " \n", - " \n", - " Jane Smith \n", - " Data Scientist \n", - " \n", - "\n", - "\n", - " \n", - " Develop text analysis module \n", - " Implement natural language processing techniques to extract key information from text documents. \n", - " \n", - " \n", - " Jane Smith \n", - " Data Scientist \n", - " \n", - " \n", - " 2022-10-15 \n", - " \n", - " \n", - " 1633660800 \n", - " Completed initial data preprocessing \n", - " true \n", - " \n", - " \n", - " \n", - " \n", - " Develop image analysis module \n", - " Implement computer vision algorithms to analyze images and extract relevant features. \n", - " \n", - " \n", - " John Doe \n", - " Project Manager \n", - " \n", - " \n", - " 2022-11-01 \n", - " \n", - " \n", - " 1635724800 \n", - " Completed image feature extraction \n", - " true \n", - " \n", - " \n", - " \n", - "\n", - "\n", - "```\n", - "-------------\n", - "{\n", - " \"Project\": {\n", - " \"name\": \"Multimodal Document Understanding Project\",\n", - " \"description\": \"This project aims to develop a system that can understand and analyze multimodal documents, including text, images, and videos.\",\n", - " \"project_url\": \"www.multimodalproject.com\",\n", - " \"team_members\": {\n", - " \"TeamMember\": [\n", - " {\n", - " \"name\": \"John Doe\",\n", - " \"role\": \"Project Manager\"\n", - " },\n", - " {\n", - " \"name\": \"Jane Smith\",\n", - " \"role\": \"Data Scientist\"\n", - " }\n", - " ]\n", - " },\n", - " \"grammars\": {\n", - " \"Task\": [\n", - " {\n", - " \"title\": \"Develop text analysis module\",\n", - " \"description\": \"Implement natural language processing techniques to extract key information from text documents.\",\n", - " \"assigned_to\": {\n", - " \"TeamMember\": {\n", - " \"name\": \"Jane Smith\",\n", - " \"role\": \"Data Scientist\"\n", - " }\n", - " },\n", - " \"due_date\": \"2022-10-15\",\n", - " \"updates\": {\n", - " \"TaskUpdate\": {\n", - " \"update_time\": 1633660800,\n", - " \"comment\": \"Completed initial data preprocessing\",\n", - " \"status\": true\n", - " }\n", - " }\n", - " },\n", - " {\n", - " \"title\": \"Develop image analysis module\",\n", - " \"description\": \"Implement computer vision algorithms to analyze images and extract relevant features.\",\n", - " \"assigned_to\": {\n", - " \"TeamMember\": {\n", - " \"name\": \"John Doe\",\n", - " \"role\": \"Project Manager\"\n", - " }\n", - " },\n", - " \"due_date\": \"2022-11-01\",\n", - " \"updates\": {\n", - " \"TaskUpdate\": {\n", - " \"update_time\": 1635724800,\n", - " \"comment\": \"Completed image feature extraction\",\n", - " \"status\": true\n", - " }\n", - " }\n", - " }\n", - " ]\n", - " }\n", - " }\n", - "}\n", - "-------------\n", - "{'before': 306, 'after': 540, 'factor': '0.8x'}\n", - "-------------\n" - ] - } - ], - "source": [ - "# You can add complex layers of grammars. You add even using Optional and Union types.\n", - "\n", - "class TeamMember(BaseModel):\n", - " name: str\n", - " role: str\n", - "\n", - "\n", - "class TaskUpdate(BaseModel):\n", - " update_time: float\n", - " comment: Optional[str] = None\n", - " status: bool\n", - "\n", - "\n", - "class Task(BaseModel):\n", - " title: str\n", - " description: str\n", - " assigned_to: List[TeamMember]\n", - " due_date: List[str]\n", - " updates: List[TaskUpdate]\n", - "\n", - "\n", - "class Project(BaseModel):\n", - " name: str\n", - " description: str\n", - " project_url: Optional[str] = None\n", - " team_members: List[TeamMember]\n", - " grammars: Task\n", - "\n", - "\n", - "with Constrain(llama_prompt) as manager:\n", - " manager.set_config(\n", - " format='xml',\n", - " return_sequence='single_response'\n", - " )\n", - "\n", - " system_context = \"\"\"You are a project manager and you are responsible for managing a project. You have to manage the project, it's grammars and other aspects. Ensure that you fill out all required fields.\"\"\"\n", - " user_message = \"\"\"Make me a project plan for a new project on multimodal document understanding projct.\"\"\"\n", - "\n", - " manager.format_prompt(placeholders={'user_message': user_message,\n", - " 'system_context': system_context},\n", - " grammars=[{\n", - " 'description': 'This format elaborates on the project and its grammars.',\n", - " 'model': [Project]},\n", - " ]\n", - " )\n", - "\n", - " prompt = manager.prompt\n", - "\n", - " llm_response = llm.request(prompt, temperature=0.01)\n", - " log(llm_response)\n", - "\n", - " response = manager.parse(llm_response)\n", - " log(response)\n", - "\n", - " log(manager.inflation_rate())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"Task\": [\n", - " {\n", - " \"title\": \"Develop text analysis module\",\n", - " \"description\": \"Implement natural language processing techniques to extract key information from text documents.\",\n", - " \"assigned_to\": {\n", - " \"TeamMember\": {\n", - " \"name\": \"Jane Smith\",\n", - " \"role\": \"Data Scientist\"\n", - " }\n", - " },\n", - " \"due_date\": \"2022-10-15\",\n", - " \"updates\": {\n", - " \"TaskUpdate\": {\n", - " \"update_time\": 1633660800,\n", - " \"comment\": \"Completed initial data preprocessing\",\n", - " \"status\": true\n", - " }\n", - " }\n", - " },\n", - " {\n", - " \"title\": \"Develop image analysis module\",\n", - " \"description\": \"Implement computer vision algorithms to analyze images and extract relevant features.\",\n", - " \"assigned_to\": {\n", - " \"TeamMember\": {\n", - " \"name\": \"John Doe\",\n", - " \"role\": \"Project Manager\"\n", - " }\n", - " },\n", - " \"due_date\": \"2022-11-01\",\n", - " \"updates\": {\n", - " \"TaskUpdate\": {\n", - " \"update_time\": 1635724800,\n", - " \"comment\": \"Completed image feature extraction\",\n", - " \"status\": true\n", - " }\n", - " }\n", - " }\n", - " ]\n", - "}\n" - ] - } - ], - "source": [ - "# The Response object allows for easy access\n", - "print(response.Project.grammars)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "```\n", - "[EventIdea]\n", - "event_name = \"Birthday Party for Girlfriend\"\n", - "event_description = \"A special celebration for my girlfriend's birthday\"\n", - "event_duration = \"3 hours\"\n", - "\n", - "[BudgetPlan]\n", - "budget = 500\n", - "items = [\"Cake\", \"Balloons\", \"Roses\", \"Ice Cream\"]\n", - "prices = [50, 20, 30, 40]\n", - "total_cost = 140\n", - "\n", - "[EventSchedule]\n", - "event_name = \"Birthday Party for Girlfriend\"\n", - "event_time = 12\n", - "event_duration = \"3 hours\"\n", - "```\n", - "-------------\n", - "{\n", - " \"EventIdea\": [\n", - " {\n", - " \"event_name\": \"Birthday Party for Girlfriend\",\n", - " \"event_description\": \"A special celebration for my girlfriend's birthday\",\n", - " \"event_duration\": \"3 hours\"\n", - " }\n", - " ],\n", - " \"BudgetPlan\": [\n", - " {\n", - " \"budget\": 500,\n", - " \"items\": [\n", - " \"Cake\",\n", - " \"Balloons\",\n", - " \"Roses\",\n", - " \"Ice Cream\"\n", - " ],\n", - " \"prices\": [\n", - " 50,\n", - " 20,\n", - " 30,\n", - " 40\n", - " ],\n", - " \"total_cost\": 140\n", - " }\n", - " ],\n", - " \"EventSchedule\": [\n", - " {\n", - " \"event_name\": \"Birthday Party for Girlfriend\",\n", - " \"event_time\": 12,\n", - " \"event_duration\": \"3 hours\"\n", - " }\n", - " ]\n", - "}\n", - "-------------\n", - "{'before': 48, 'after': 198, 'factor': '3.1x'}\n", - "-------------\n" - ] - } - ], - "source": [ - "# You can add multiple grammars to the same prompt. NOT RECOMMENDED.\n", - "\n", - "class EventIdea(BaseModel):\n", - " event_name: str\n", - " event_description: str\n", - " event_duration: str\n", - "\n", - "\n", - "class BudgetPlan(BaseModel):\n", - " budget: float\n", - " items: List[str]\n", - " prices: List[int]\n", - " total_cost: int\n", - "\n", - "\n", - "class EventSchedule(BaseModel):\n", - " event_name: str\n", - " event_time: float\n", - " event_duration: str\n", - "\n", - "\n", - "prompt = \"I am hosting a birthday party for my girlfriend tomorrow. I want to buy a cake, balloons, some roses and ice cream. I have a budget of 500$. Can you create a sample event schedule and budget plan for me?.\"\n", - "\n", - "with Constrain(prompt) as manager:\n", - " manager.set_config(\n", - " format=\"toml\",\n", - " return_sequence=\"multi_response\",\n", - " )\n", - "\n", - " manager.format_prompt(\n", - " grammars=[\n", - " {\"task_description\": \"Brainstorming Event Ideas\", \"model\": EventIdea},\n", - " {\n", - " \"task_description\": \"Budget Planning And Activity Planning\",\n", - " \"model\": [BudgetPlan, EventSchedule],\n", - " },\n", - " ],\n", - " )\n", - "\n", - " prompt = manager.prompt\n", - "\n", - " llm_response = llm.request(prompt, temperature=0.01)\n", - " log(llm_response)\n", - "\n", - " response = manager.parse(llm_response)\n", - " log(response)\n", - "\n", - " log(manager.inflation_rate())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Grammars" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "> \"GBNF (GGML BNF) is a format for defining formal grammars to constrain model outputs in llama.cpp. For example, you can use it to force the model to generate valid JSON, or speak only in emojis.\"\n", - "\n", - "Read more about it here: https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from grammarflow.grammars.gnbf import GNBF" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "grammar = GNBF(Project).generate_grammar()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "root ::= project ws\n", - "project ::= \"{\" ws \"\\\"name\\\":\" ws string \",\" ws \"\\\"description\\\":\" ws string \",\" ws \"\\\"project-url\\\":\" ws string \",\" ws \"\\\"team-members\\\":\" ws teammember \",\" ws \"\\\"grammars\\\":\" ws grammars \"}\" ws\n", - "ws ::= [ \\t\\n]*\n", - "string ::= \"\\\"\" (\n", - " [^\"\\\\] |\n", - " \"\\\\\" ([\"\\\\/bfnrt] | \"u\" [0-9a-fa-f] [0-9a-fa-f] [0-9a-fa-f] [0-9a-fa-f])\n", - " )* \"\\\"\"\n", - "teammember ::= \"{\" ws \"\\\"name\\\":\" ws string \",\" ws \"\\\"role\\\":\" ws string \"}\" ws\n", - "number ::= (\"-\"? ([0-9] | [1-9] [0-9]*)) (\".\" [0-9]+)? ([ee] [-+]? [0-9]+)?\n", - "taskupdate ::= \"{\" ws \"\\\"update-time\\\":\" ws number \",\" ws \"\\\"comment\\\":\" ws string \",\" ws \"\\\"status\\\":\" ws status \"}\" ws\n", - "array ::= \"[\" ws (\n", - " due-date-value\n", - " (\",\" ws due-date-value)*\n", - " )? \"]\" ws\n", - "due-date-value ::= string\n", - "task ::= \"{\" ws \"\\\"title\\\":\" ws string \",\" ws \"\\\"description\\\":\" ws string \",\" ws \"\\\"assigned-to\\\":\" ws teammember \",\" ws \"\\\"due-date\\\":\" ws array \",\" ws \"\\\"updates\\\":\" ws taskupdate \"}\" ws\n", - "-------------\n" - ] - } - ], - "source": [ - "log(grammar)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "from_string grammar:\n", - "root ::= project ws \n", - "project ::= [{] ws [\"] [n] [a] [m] [e] [\"] [:] ws string [,] ws [\"] [d] [e] [s] [c] [r] [i] [p] [t] [i] [o] [n] [\"] [:] ws string [,] ws [\"] [p] [r] [o] [j] [e] [c] [t] [-] [u] [r] [l] [\"] [:] ws string [,] ws [\"] [t] [e] [a] [m] [-] [m] [e] [m] [b] [e] [r] [s] [\"] [:] ws teammember [,] ws [\"] [g] [r] [a] [m] [m] [a] [r] [s] [\"] [:] ws grammars [}] ws \n", - "ws ::= ws_6 \n", - "string ::= [\"] string_9 [\"] \n", - "teammember ::= [{] ws [\"] [n] [a] [m] [e] [\"] [:] ws string [,] ws [\"] [r] [o] [l] [e] [\"] [:] ws string [}] ws \n", - "print_grammar: error printing grammar: malformed rule, does not end with LLAMA_GRETYPE_END: 5\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Using llama.cpp, we can verify if our grammar string is accepted.\n", - "GNBF.verify_grammar(grammar)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.18" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}