Package: langchain-decorators
LangChain Decorators ✨lanchchain decorators is a layer on top of LangChain that provides syntactic sugar 🍭 for writing custom langchain prompts and chainsNote:This is an unofficial addon to the langchain library. It's not trying to compete, just to make using it easier. Lot's of ideas here are totally opinionatedHere is a simple example of a code written withLangChain Decorators ✨@llm_promptdefwrite_me_short_post(topic:str,platform:str="twitter",audience:str="developers")->str:"""Write me a short header for my post about {topic} for {platform} platform.It should be for {audience} audience.(Max 15 words)"""return# run it naturalywrite_me_short_post(topic="starwars")# orwrite_me_short_post(topic="starwars",platform="redit")Main principles and benefits:morepythonicway of writing codewrite multiline prompts that won't break your code flow with indentationmaking use of IDE in-built support forhinting,type checkingandpopup with docsto quickly peek in the function to see the prompt, parameters it consumes etc.leverage all the power of 🦜🔗 LangChain ecosystemadding support foroptional parameterseasily share parameters between the prompts by binding them to one classQuick startInstallationExamplesPrompt declarationsDocumenting your promptChat messages promptOptional sectionsOutput parsersLLM functions (OpenAI functions)Functions providerDynamic function schemasSimplified streamingAutomatic LLM selectionMore complex structuresBinding the prompt to an objectDefining custom settingsDebuggingPassing a memory, callback, stop etc.OtherMore examplesContributingQuick startInstallationpipinstalllangchain_decoratorsExamplesGood idea on how to start is to review the examples here:jupyter notebookcolab notebookPrompt declarationsBy default the prompt is the whole function docs, unless you mark your promptDocumenting your promptWe can specify what part of our docs is the prompt definition, by specifying a code block withlanguage tag@llm_promptdefwrite_me_short_post(topic:str,platform:str="twitter",audience:str="developers"):"""Here is a good way to write a prompt as part of a function docstring, with additional documentation for devs.It needs to be a code block, marked as a `<prompt>` language```<prompt>Write me a short header for my post about {topic} for {platform} platform.It should be for {audience} audience.(Max 15 words)```Now only the code block above will be used as a prompt, and the rest of the docstring will be used as a description for developers.(It also has a nice benefit that IDE (like VS code) will display the prompt properly (not trying to parse it as markdown, and thus not showing new lines properly))"""returnChat messages promptFor chat models is very useful to define prompt as a set of message templates... here is how to do it:@llm_promptdefsimulate_conversation(human_input:str,agent_role:str="a pirate"):"""## System message- note the `:system` sufix inside the <prompt:_role_> tag```<prompt:system>You are a {agent_role} hacker. You must act like one.You reply always in code, using python or javascript code block...for example:... do not reply with anything else.. just with code - respecting your role.```# human message(we are using the real role that are enforced by the LLM - GPT supports system, assistant, user)``` <prompt:user>Helo, who are you```a reply:``` <prompt:assistant>\``` python <<- escaping inner code block with \ that should be part of the promptdef hello():print("Argh... hello you pesky pirate")\``````we can also add some history using placeholder```<prompt:placeholder>{history}``````<prompt:user>{human_input}```Now only the code block above will be used as a prompt, and the rest of the docstring will be used as a description for developers.(It also has a nice benefit that IDE (like VS code) will display the prompt properly (not trying to parse it as markdown, and thus not showing new lines properly))"""passthe roles here are model native roles (assistant, user, system for chatGPT)Optional sectionsyou can define a whole section of your prompt that should be optionalif any input in the section is missing, the whole section won't be renderedthe syntax for this is as follows:@llm_promptdefprompt_with_optional_partials():"""this text will be rendered always, but{? anything inside this block will be rendered only if all the {value}s parameters are not empty (None | "")   ?}you can also place it in between the wordsthis too will be rendered{? , butthis  block will be rendered only if {this_value} and {this_value}are not empty?} !"""Output parsersllm_prompt decorator natively tries to detect the best output parser based on the output type. (if not set, it returns the raw string)list, dict and pydantic outputs are also supported natively (automatically)# this code example is complete and should run as it isfromlangchain_decoratorsimportllm_prompt@llm_promptdefwrite_name_suggestions(company_business:str,count:int)->list:""" Write me {count} good name suggestions for company that {company_business}"""passwrite_name_suggestions(company_business="sells cookies",count=5)LLM functionscurrently supported only for the latest OpenAI chat modelsall you need to do is annotate your function with the @llm_function decorator.This will parse the description for LLM (first coherent paragraph is considered as function description)and aso parameter descriptions (Google, Numpy and Spihnx notations are supported for now)by default the docstring format is automatically resolved, but setting the format of the docstring can speed things up a bit.
-auto(default): the format is automatically inferred from the docstring
-google: the docstring is parsed as markdown (seeGoogle docstring format)
-numpy: the docstring is parsed as markdown (seeNumpy docstring format)
-sphinx: the docstring is parsed as sphinx format (seeSphinx docstring format)To annotate an"enum"like argument, you can use this "typescript" like format:["value_a" | "value_b"]... if will be parsed out.
This text will be a part of a description too... if you dont want it, you can use this notation as a type notation.
Example:Args:
    message_type (["email" | "sms"]): type of a message  / channel how to send the messageThen you pass these functions as arguments to and@llm_prompt(the argument must be namedfunctions‼️)
here you can pass any @llm_function there or a native LangChain toolhere is how to use it:fromlangchain.agentsimportload_toolsfromlangchian_decoratorsimportllm_function,llm_prompt,GlobalSettings@llm_functiondefsend_message(message:str,addressee:str=None,message_type:str="email"):""" Use this if user asks to send some messageArgs:message (str): message text to sendaddressee (str): email of the adressese... in format firstName.lastName@company.commessage_type (str, optional): enum: ["email"|"whatsapp"]"""ifmessage_type=="email":send_email(addressee,message)elifmessage_type=="whatsapp":send_whatsapp(addressee,message)# load some other tools from langchainlist_of_other_tools=load_tools(tool_names=[...],llm=GlobalSettings.get_current_settings().default_llm)@llm_promptdefdo_what_user_asks_for(user_input:str,functions:List[Union[Callable,BaseTool]]):"""``` <prompt:system>Your role is to be a helpful asistant.`````` <prompt:user>{user_input}```"""user_input="Yo, send an email to John Smith that I will be late for the meeting"result=do_what_user_asks_for(user_input=user_input,functions=[send_message,*list_of_other_tools])ifresult.is_function_call:result.execute()else:print(result.output_text)Additionally you can also add afunction_callargument to your LLM prompt to control GPT behavior.if you set the value to "none" - it will disable the function call for the moment, but it can still see them (useful do to some reasoning/planning before calling the function)if you set the value to "auto" - GPT will choose to use or to to use the functionsif you set the value to a name of function / or the function it self (decorators will handle resolving the same name as used in schema) it will force GPT to use that functionIf you use functions argument, the output will be alwaysOutputWithFunctionCallclassOutputWithFunctionCall(BaseModel):output_text:stroutput:Tfunction_name:str=Nonefunction_arguments:Union[Dict[str,Any],str,None]function:Callable=Nonefunction_async:Callable=None@propertydefis_function_call(self):...@propertydefsupport_async(self):...@propertydefsupport_sync(self):...asyncdefexecute_async(self):"""Executes the function asynchronously."""...defexecute(self):""" Executes the function synchronously.If the function is async, it will be executed in a event loop."""...defto_function_message(self,result=None):"""Converts the result to a FunctionMessage...you can override the result collected via execute with your own"""...If you want to see how the schema has been build, you can useget_function_schemamethod that is added to the function by the decorator:fromlangchain_decoratorsimportget_function_schema@llm_functiondefmy_func(arg1:str):...f_schema=get_function_schema(my_func.get_function_schema)print(f_schema)In order to add the result to memory / agent_scratchpad you can useto_function_messageto generate a FunctionMessage that LLM will interpret as a Tool/Function resultFunctions providerFunctions provider enables you to provide set of llm functions more dynamically, for example list of functions - based on the input.
It also enables you to give a unique name to each function for this LLM run. This might be useful for two reasons:avoid naming conflicts, if you are combining multiple general purpose functionsfurther guidance/hinting of LLM modelDynamic function schemasFunction schemas (and especially their descriptions) are crucial tools to guide LLM. If you enable dynamic function declaration, you can (re)use the same prompt attributes for the main prompt also in the llm_function scheme:@llm_function(dynamic_schema=True)defdb_search(query_input:str):"""This function is useful to search in our database.{?Here are some examples of data available:{closest_examples}?}"""@llm_promptdefrun_agent(query_input:str,closest_examples:str,functions):"""Help user. Use a function when appropriate"""closest_examples=get_closest_examples()run_agent(query_input,closest_examples,functions=[db_search,...])this is just for illustration, fully executable example is availablehere, in code examplesSimplified streamingIf we want to leverage streaming:we need to define prompt as async functionturn on the streaming on the decorator, or we can define PromptType with streaming oncapture the stream using StreamingContextThis way we just mark which prompt should be streamed, not needing to tinker with what LLM should we use, passing around the creating and distribute streaming handler into particular part of our chain... just turn the streaming on/off on prompt/prompt type...The streaming will happen only if we call it in streaming context ... there we can define a simple function to handle the stream# this code example is complete and should run as it isfromlangchain_decoratorsimportStreamingContext,llm_prompt# this will mark the prompt for streaming (useful if we want stream just some prompts in our app... but don't want to pass distribute the callback handlers)# note that only async functions can be streamed (will get an error if it's not)@llm_prompt(capture_stream=True)asyncdefwrite_me_short_post(topic:str,platform:str="twitter",audience:str="developers"):"""Write me a short header for my post about {topic} for {platform} platform.It should be for {audience} audience.(Max 15 words)"""pass# just an arbitrary  function to demonstrate the streaming... wil be some websockets code in the real worldtokens=[]defcapture_stream_func(new_token:str):tokens.append(new_token)# if we want to capture the stream, we need to wrap the execution into StreamingContext...# this will allow us to capture the stream even if the prompt call is hidden inside higher level method# only the prompts marked with capture_stream will be captured herewithStreamingContext(stream_to_stdout=True,callback=capture_stream_func):result=awaitrun_prompt()print("Stream finished ... we can distinguish tokens thanks to alternating colors")print("\nWe've captured",len(tokens),"tokens🎉\n")print("Here is the result:")print(result)Automatic LLM selectionIn real life there might be situations, where the context would grow over the window of the base model you're using (for example long chat history)...
But since this might happen only some times, it would be great if only in this scenario the (usually more expensive) model with bigger context window would be used, and otherwise we'd use the cheaper one.Now you can do it with LlmSelectorfromlangchain_decoratorsimportLlmSelectormy_llm_selector=LlmSelector(generation_min_tokens=0,# how much token at min. I for generation I want to have as a bufferprompt_to_generation_ratio=1/3# what percentage of the prompt length should be used for generation buffer)\.with_llm_rule(ChatGooglePalm(),max_tokens=512)\# ... if you want to use LLM whose window is not defined in langchain_decorators.common.MODEL_LIMITS (only OpenAI and Anthropic are there).with_llm(ChatOpenAI(model="gpt-3.5-turbo"))\# these models are known, therefore we can just pass them and the max window will be resolved.with_llm(ChatOpenAI(model="gpt-3.5-turbo-16k-0613"))\.with_llm(ChatOpenAI(model="claude-v1.3-100k"))This class allows you to define a sequence of LLMs with a rule based on the length of the prompt, and expected generation length... and only after the threshold will be passed, the more expensive model will be used automatically.You can define it into GlobalSettings:langchain_decorators.GlobalSettings.define_settings(llm_selector=my_llm_selector# pass the selector into global settings)Note:as of version v0.0.10 you there the LlmSelector is in the default settings predefined.
You can override it by providing you own, or setting up the default LLM or default streaming LLMOr into specific prompt type:fromlangchain_decoratorsimportPromptTypesclassMyCustomPromptTypes(PromptTypes):MY_TUBO_PROMPT=PromptTypeSettings(llm_selector=my_llm_selector)More complex structuresFor dict / pydantic you need to specify the formatting instructions...
this can be tedious, that's why you can let the output parser generate you the instructions based on the model (pydantic)fromlangchain_decoratorsimportllm_promptfrompydanticimportBaseModel,FieldclassTheOutputStructureWeExpect(BaseModel):name:str=Field(description="The name of the company")headline:str=Field(description="The description of the company (for landing page)")employees:list[str]=Field(description="5-8 fake employee names with their positions")@llm_prompt()deffake_company_generator(company_business:str)->TheOutputStructureWeExpect:""" Generate a fake company that {company_business}{FORMAT_INSTRUCTIONS}"""returncompany=fake_company_generator(company_business="sells cookies")# print the result nicely formattedprint("Company name: ",company.name)print("company headline: ",company.headline)print("company employees: ",company.employees)Binding the prompt to an objectfrompydanticimportBaseModelfromlangchain_decoratorsimportllm_promptclassAssistantPersonality(BaseModel):assistant_name:strassistant_role:strfield:str@propertydefa_property(self):return"whatever"defhello_world(self,function_kwarg:str=None):"""We can reference any {field} or {a_property} inside our prompt... and combine it with {function_kwarg} in the method"""@llm_promptdefintroduce_your_self(self)->str:"""``` <prompt:system>You are an assistant named {assistant_name}.Your role is to act as {assistant_role}``````<prompt:user>Introduce your self (in less than 20 words)```"""personality=AssistantPersonality(assistant_name="John",assistant_role="a pirate")print(personality.introduce_your_self(personality))Defining custom settingsHere we are just marking a function as a prompt withllm_promptdecorator, turning it effectively into a LLMChain. Instead of running itStandard LLMchain takes much more init parameter than just inputs_variables and prompt... here is this implementation detail hidden in the decorator.
Here is how it works:UsingGlobal settings:# define global settings for all prompty (if not set - chatGPT is the current default)fromlangchain_decoratorsimportGlobalSettingsGlobalSettings.define_settings(default_llm=ChatOpenAI(temperature=0.0),thisisdefault...canchangeitheregloballydefault_streaming_llm=ChatOpenAI(temperature=0.0,streaming=True),thisisdefault...canchangeithereforall...willbeusedforstreaming)Using predefinedprompt types#You can change the default prompt typesfromlangchain_decoratorsimportPromptTypes,PromptTypeSettingsPromptTypes.AGENT_REASONING.llm=ChatOpenAI()# Or you can just define your own ones:classMyCustomPromptTypes(PromptTypes):GPT4=PromptTypeSettings(llm=ChatOpenAI(model="gpt-4"))@llm_prompt(prompt_type=MyCustomPromptTypes.GPT4)defwrite_a_complicated_code(app_idea:str)->str:...Define the settingsdirectly in the decoratorfromlangchain.llmsimportOpenAI@llm_prompt(llm=OpenAI(temperature=0.7),stop_tokens=["\nObservation"],...)defcreative_writer(book_title:str)->str:...Passing a memory, callback, stop, etcTo pass any of these, just declare them in the function (or use kwargs to pass anything)(They do not necessarily need to be declared, but it is a good practice if you are going to use them)@llm_prompt()asyncdefwrite_me_short_post(topic:str,platform:str="twitter",memory:SimpleMemory=None):"""{history_key}Write me a short header for my post about {topic} for {platform} platform.It should be for {audience} audience.(Max 15 words)"""passawaitwrite_me_short_post(topic="old movies")DebuggingLogging to consoleThere are several options how to control the outputs logged into console.
The easiest way is to define ENV variable:LANGCHAIN_DECORATORS_VERBOSEand set it to "true"You can also control this programmatically by defining your global settings as shownhereThe last option is to control it per each case, simply by turing on verbose mode on prompt:@llm_prompt(verbose=True)
def your_prompt(param1):
  ...Using PromptWatch.ioPromptWatch io is a platform to track and trace details about everything that is going on in langchain executions.
It allows a single line drop in integration, just by wrapping your entry point code inwith PromptWatch():
    run_your_code()Learn more about PromptWatch here:www.promptwatch.ioOtherthis project is dependant onlangchain(obviously)as well as onpromptwatch, which make it easy to track and store to logs, track changes in prompts and compare them by running unit tests over the prompts...More examplesthese and few more examples are also available in theexamples notebook hereincluding theReAct Agent re-implementationusing purely langchain decoratorsContributingfeedback, contributions and PR are welcomed 🙏