From 313f1a7592bc4d4fe49384f8e43b3e80cdbafe8c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 11:24:07 -0700 Subject: [PATCH 01/51] feat: blueprints --- blueprints/function_calling_blueprint.py | 172 +++++++++++ examples/function_calling_filter_pipeline.py | 295 +++++-------------- main.py | 1 - utils/main.py | 9 +- 4 files changed, 248 insertions(+), 229 deletions(-) create mode 100644 blueprints/function_calling_blueprint.py diff --git a/blueprints/function_calling_blueprint.py b/blueprints/function_calling_blueprint.py new file mode 100644 index 0000000..aeae241 --- /dev/null +++ b/blueprints/function_calling_blueprint.py @@ -0,0 +1,172 @@ +from typing import List, Optional +from pydantic import BaseModel +from schemas import OpenAIChatMessage +import os +import requests +import json + +from utils.main import ( + get_last_user_message, + add_or_update_system_message, + get_tools_specs, +) + + +class Pipeline: + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + + # Valves for function calling + OPENAI_API_BASE_URL: str + OPENAI_API_KEY: str + TASK_MODEL: str + TEMPLATE: str + + def __init__(self): + # Pipeline filters are only compatible with Open WebUI + # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. + self.type = "filter" + + # Assign a unique identifier to the pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + self.id = "function_calling_blueprint" + self.name = "Function Calling Blueprint" + + # Initialize valves + self.valves = self.Valves( + **{ + "pipelines": ["*"], # Connect to all pipelines + "OPENAI_API_BASE_URL": os.getenv( + "OPENAI_API_BASE_URL", "https://api.openai.com/v1" + ), + "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"), + "TASK_MODEL": os.getenv("TASK_MODEL", "gpt-3.5-turbo"), + "TEMPLATE": """Use the following context as your learned knowledge, inside XML tags. + + {{CONTEXT}} + + +When answer to user: +- If you don't know, just say that you don't know. +- If you don't know when you are not sure, ask for clarification. +Avoid mentioning that you obtained the information from the context. +And answer according to the language of the user's question.""", + } + ) + + async def on_startup(self): + # This function is called when the server is started. + print(f"on_startup:{__name__}") + pass + + async def on_shutdown(self): + # This function is called when the server is stopped. + print(f"on_shutdown:{__name__}") + pass + + async def inlet(self, body: dict, user: Optional[dict] = None) -> dict: + # If title generation is requested, skip the function calling filter + if body.get("title", False): + return body + + print(f"pipe:{__name__}") + print(user) + + # Get the last user message + user_message = get_last_user_message(body["messages"]) + + # Get the tools specs + tools_specs = get_tools_specs(self.tools) + + # System prompt for function calling + fc_system_prompt = ( + f"Tools: {json.dumps(tools_specs, indent=2)}" + + """ +If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text." +""" + ) + + r = None + try: + # Call the OpenAI API to get the function response + r = requests.post( + url=f"{self.valves.OPENAI_API_BASE_URL}/chat/completions", + json={ + "model": self.valves.TASK_MODEL, + "messages": [ + { + "role": "system", + "content": fc_system_prompt, + }, + { + "role": "user", + "content": "History:\n" + + "\n".join( + [ + f"{message['role']}: {message['content']}" + for message in body["messages"][::-1][:4] + ] + ) + + f"Query: {user_message}", + }, + ], + # TODO: dynamically add response_format? + # "response_format": {"type": "json_object"}, + }, + headers={ + "Authorization": f"Bearer {self.valves.OPENAI_API_KEY}", + "Content-Type": "application/json", + }, + stream=False, + ) + r.raise_for_status() + + response = r.json() + content = response["choices"][0]["message"]["content"] + + # Parse the function response + if content != "": + result = json.loads(content) + print(result) + + # Call the function + if "name" in result: + function = getattr(self.tools, result["name"]) + function_result = None + try: + function_result = function(**result["parameters"]) + except Exception as e: + print(e) + + # Add the function result to the system prompt + if function_result: + system_prompt = self.valves.TEMPLATE.replace( + "{{CONTEXT}}", function_result + ) + + print(system_prompt) + messages = add_or_update_system_message( + system_prompt, body["messages"] + ) + + # Return the updated messages + return {**body, "messages": messages} + + except Exception as e: + print(f"Error: {e}") + + if r: + try: + print(r.json()) + except: + pass + + return body diff --git a/examples/function_calling_filter_pipeline.py b/examples/function_calling_filter_pipeline.py index 43bae4d..8db3da5 100644 --- a/examples/function_calling_filter_pipeline.py +++ b/examples/function_calling_filter_pipeline.py @@ -1,231 +1,80 @@ -from typing import List, Optional -from pydantic import BaseModel -from schemas import OpenAIChatMessage import os import requests -import json - -from utils.main import ( - get_last_user_message, - add_or_update_system_message, - get_function_specs, -) -from typing import Literal +from typing import Literal, List, Optional +from blueprints.function_calling_blueprint import Pipeline as FunctionCallingBlueprint -class Pipeline: +class Pipeline(FunctionCallingBlueprint): + class Valves(FunctionCallingBlueprint.Valves): + # Add your custom parameters here + OPENWEATHERMAP_API_KEY: str = "" + pass + + class Tools: + def __init__(self, pipeline) -> None: + self.pipeline = pipeline + + def get_current_weather( + self, + location: str, + unit: Literal["metric", "fahrenheit"] = "fahrenheit", + ) -> str: + """ + Get the current weather for a location. If the location is not found, return an empty string. + + :param location: The location to get the weather for. + :param unit: The unit to get the weather in. Default is fahrenheit. + :return: The current weather for the location. + """ + + # https://openweathermap.org/api + + if self.pipeline.valves.OPENWEATHERMAP_API_KEY == "": + return "OpenWeatherMap API Key not set, ask the user to set it up." + else: + units = "imperial" if unit == "fahrenheit" else "metric" + params = { + "q": location, + "appid": self.pipeline.valves.OPENWEATHERMAP_API_KEY, + "units": units, + } + + response = requests.get( + "http://api.openweathermap.org/data/2.5/weather", params=params + ) + response.raise_for_status() # Raises an HTTPError for bad responses + data = response.json() + + weather_description = data["weather"][0]["description"] + temperature = data["main"]["temp"] + + return f"{location}: {weather_description.capitalize()}, {temperature}°{unit.capitalize()[0]}" + + def calculator(self, equation: str) -> str: + """ + Calculate the result of an equation. + + :param equation: The equation to calculate. + """ + + # Avoid using eval in production code + # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html + try: + result = eval(equation) + return f"{equation} = {result}" + except Exception as e: + print(e) + return "Invalid equation" + def __init__(self): - # Pipeline filters are only compatible with Open WebUI - # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. - self.type = "filter" - - # Assign a unique identifier to the pipeline. - # The identifier must be unique across all pipelines. - # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "function_calling_filter_pipeline" - self.name = "Function Calling Filter" - - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - - # Valves for function calling - OPENAI_API_BASE_URL: str - OPENAI_API_KEY: str - TASK_MODEL: str - TEMPLATE: str - - OPENWEATHERMAP_API_KEY: str = "" - - # Initialize valves - self.valves = Valves( + super().__init__() + self.id = "my_tools_pipeline" + self.name = "My Tools Pipeline" + self.valves = self.Valves( **{ + **self.valves.model_dump(), "pipelines": ["*"], # Connect to all pipelines - "OPENAI_API_BASE_URL": "https://api.openai.com/v1", - "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY"), - "TASK_MODEL": "gpt-3.5-turbo", - "TEMPLATE": """Use the following context as your learned knowledge, inside XML tags. - - {{CONTEXT}} - - -When answer to user: -- If you don't know, just say that you don't know. -- If you don't know when you are not sure, ask for clarification. -Avoid mentioning that you obtained the information from the context. -And answer according to the language of the user's question.""", - } + "OPENWEATHERMAP_API_KEY": os.getenv("OPENWEATHERMAP_API_KEY", ""), + }, ) - - class Functions: - def __init__(self, pipeline) -> None: - self.pipeline = pipeline - - def get_current_weather( - self, - location: str, - unit: Literal["metric", "fahrenheit"] = "fahrenheit", - ) -> str: - """ - Get the current weather for a location. If the location is not found, return an empty string. - - :param location: The location to get the weather for. - :param unit: The unit to get the weather in. Default is fahrenheit. - :return: The current weather for the location. - """ - - # https://openweathermap.org/api - - if self.pipeline.valves.OPENWEATHERMAP_API_KEY == "": - return "OpenWeatherMap API Key not set, ask the user to set it up." - else: - units = "imperial" if unit == "fahrenheit" else "metric" - params = { - "q": location, - "appid": self.pipeline.valves.OPENWEATHERMAP_API_KEY, - "units": units, - } - - response = requests.get( - "http://api.openweathermap.org/data/2.5/weather", params=params - ) - response.raise_for_status() # Raises an HTTPError for bad responses - data = response.json() - - weather_description = data["weather"][0]["description"] - temperature = data["main"]["temp"] - - return f"{location}: {weather_description.capitalize()}, {temperature}°{unit.capitalize()[0]}" - - def calculator(self, equation: str) -> str: - """ - Calculate the result of an equation. - - :param equation: The equation to calculate. - """ - - # Avoid using eval in production code - # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html - try: - result = eval(equation) - return f"{equation} = {result}" - except Exception as e: - print(e) - return "Invalid equation" - - self.functions = Functions(self) - - async def on_startup(self): - # This function is called when the server is started. - print(f"on_startup:{__name__}") - pass - - async def on_shutdown(self): - # This function is called when the server is stopped. - print(f"on_shutdown:{__name__}") - pass - - async def inlet(self, body: dict, user: Optional[dict] = None) -> dict: - # If title generation is requested, skip the function calling filter - if body.get("title", False): - return body - - print(f"pipe:{__name__}") - print(user) - - # Get the last user message - user_message = get_last_user_message(body["messages"]) - - # Get the function specs - function_specs = get_function_specs(self.functions) - - # System prompt for function calling - fc_system_prompt = ( - f"Functions: {json.dumps(function_specs, indent=2)}" - + """ -If a function doesn't match the query, return an empty string. Else, pick a function, fill in the parameters from the function's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text." -""" - ) - - r = None - try: - # Call the OpenAI API to get the function response - r = requests.post( - url=f"{self.valves.OPENAI_API_BASE_URL}/chat/completions", - json={ - "model": self.valves.TASK_MODEL, - "messages": [ - { - "role": "system", - "content": fc_system_prompt, - }, - { - "role": "user", - "content": "History:\n" - + "\n".join( - [ - f"{message['role']}: {message['content']}" - for message in body["messages"][::-1][:4] - ] - ) - + f"Query: {user_message}", - }, - ], - # TODO: dynamically add response_format? - # "response_format": {"type": "json_object"}, - }, - headers={ - "Authorization": f"Bearer {self.valves.OPENAI_API_KEY}", - "Content-Type": "application/json", - }, - stream=False, - ) - r.raise_for_status() - - response = r.json() - content = response["choices"][0]["message"]["content"] - - # Parse the function response - if content != "": - result = json.loads(content) - print(result) - - # Call the function - if "name" in result: - function = getattr(self.functions, result["name"]) - function_result = None - try: - function_result = function(**result["parameters"]) - except Exception as e: - print(e) - - # Add the function result to the system prompt - if function_result: - system_prompt = self.valves.TEMPLATE.replace( - "{{CONTEXT}}", function_result - ) - - print(system_prompt) - messages = add_or_update_system_message( - system_prompt, body["messages"] - ) - - # Return the updated messages - return {**body, "messages": messages} - - except Exception as e: - print(f"Error: {e}") - - if r: - try: - print(r.json()) - except: - pass - - return body + self.tools = self.Tools(self) diff --git a/main.py b/main.py index f095089..ebd41c7 100644 --- a/main.py +++ b/main.py @@ -203,7 +203,6 @@ async def get_models(): Returns the available pipelines """ app.state.PIPELINES = get_all_pipelines() - return { "data": [ { diff --git a/utils/main.py b/utils/main.py index 7a8b55b..f5830b9 100644 --- a/utils/main.py +++ b/utils/main.py @@ -80,12 +80,11 @@ def doc_to_dict(docstring): return ret_dict -def get_function_specs(functions) -> List[dict]: - +def get_tools_specs(tools) -> List[dict]: function_list = [ - {"name": func, "function": getattr(functions, func)} - for func in dir(functions) - if callable(getattr(functions, func)) and not func.startswith("__") + {"name": func, "function": getattr(tools, func)} + for func in dir(tools) + if callable(getattr(tools, func)) and not func.startswith("__") ] specs = [] From 74e5c067e5b21507a57077ff0d69f42baa9f4537 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 11:29:08 -0700 Subject: [PATCH 02/51] example: fc scaffold --- examples/function_calling_scaffold.py | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 examples/function_calling_scaffold.py diff --git a/examples/function_calling_scaffold.py b/examples/function_calling_scaffold.py new file mode 100644 index 0000000..aae6e50 --- /dev/null +++ b/examples/function_calling_scaffold.py @@ -0,0 +1,28 @@ +from blueprints.function_calling_blueprint import Pipeline as FunctionCallingBlueprint + + +class Pipeline(FunctionCallingBlueprint): + class Valves(FunctionCallingBlueprint.Valves): + # Add your custom parameters here + pass + + class Tools: + def __init__(self, pipeline) -> None: + self.pipeline = pipeline + + # Add your custom tools here + # Please refer to function_calling_filter_pipeline.py for an example + # Pure Python code can be added here + pass + + def __init__(self): + super().__init__() + self.id = "my_tools_pipeline" + self.name = "My Tools Pipeline" + self.valves = self.Valves( + **{ + **self.valves.model_dump(), + "pipelines": ["*"], # Connect to all pipelines + }, + ) + self.tools = self.Tools(self) From 5b8b9d8f6d96a1692d234ef3f8ba224212375537 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 11:36:31 -0700 Subject: [PATCH 03/51] refac: valves --- examples/anthropic_manifold_pipeline.py | 10 ++-- examples/cohere_manifold_pipeline.py | 10 ++-- examples/conversation_turn_limit_filter.py | 30 +++++----- examples/detoxify_filter_pipeline.py | 24 ++++---- examples/example_pipeline.py | 4 ++ examples/filter_pipeline.py | 28 ++++----- examples/langfuse_filter_pipeline.py | 37 ++++++------ examples/libretranslate_filter_pipeline.py | 51 ++++++++-------- examples/litellm_manifold_pipeline.py | 9 +-- .../litellm_subprocess_manifold_pipeline.py | 14 ++--- examples/ollama_manifold_pipeline.py | 9 +-- examples/rate_limit_filter_pipeline.py | 59 ++++++++++--------- examples/wikipedia_pipeline.py | 8 +-- 13 files changed, 153 insertions(+), 140 deletions(-) diff --git a/examples/anthropic_manifold_pipeline.py b/examples/anthropic_manifold_pipeline.py index 9236db4..1cfcb14 100644 --- a/examples/anthropic_manifold_pipeline.py +++ b/examples/anthropic_manifold_pipeline.py @@ -19,15 +19,17 @@ import requests class Pipeline: + class Valves(BaseModel): + ANTHROPIC_API_KEY: str = "" + def __init__(self): self.type = "manifold" self.id = "anthropic" self.name = "anthropic/" - class Valves(BaseModel): - ANTHROPIC_API_KEY: str - - self.valves = Valves(**{"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY")}) + self.valves = self.Valves( + **{"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY")} + ) self.client = Anthropic(api_key=self.valves.ANTHROPIC_API_KEY) def get_anthropic_models(self): diff --git a/examples/cohere_manifold_pipeline.py b/examples/cohere_manifold_pipeline.py index 896f0f2..20bc5ec 100644 --- a/examples/cohere_manifold_pipeline.py +++ b/examples/cohere_manifold_pipeline.py @@ -18,16 +18,16 @@ import requests class Pipeline: + class Valves(BaseModel): + COHERE_API_BASE_URL: str = "https://api.cohere.com/v1" + COHERE_API_KEY: str = "" + def __init__(self): self.type = "manifold" self.id = "cohere" self.name = "cohere/" - class Valves(BaseModel): - COHERE_API_BASE_URL: str = "https://api.cohere.com/v1" - COHERE_API_KEY: str - - self.valves = Valves(**{"COHERE_API_KEY": os.getenv("COHERE_API_KEY")}) + self.valves = self.Valves(**{"COHERE_API_KEY": os.getenv("COHERE_API_KEY")}) self.pipelines = self.get_cohere_models() diff --git a/examples/conversation_turn_limit_filter.py b/examples/conversation_turn_limit_filter.py index ca6c264..54e9483 100644 --- a/examples/conversation_turn_limit_filter.py +++ b/examples/conversation_turn_limit_filter.py @@ -6,6 +6,20 @@ import time class Pipeline: + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + + # Valves for conversation turn limiting + target_user_roles: List[str] = ["user"] + max_turns: Optional[int] = None + def __init__(self): # Pipeline filters are only compatible with Open WebUI # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. @@ -17,21 +31,7 @@ class Pipeline: self.id = "conversation_turn_limit_filter_pipeline" self.name = "Conversation Turn Limit Filter" - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - - # Valves for conversation turn limiting - target_user_roles: List[str] = ["user"] - max_turns: Optional[int] = None - - self.valves = Valves( + self.valves = self.Valves( **{ "pipelines": os.getenv("CONVERSATION_TURN_PIPELINES", "*").split(","), "max_turns": 10, diff --git a/examples/detoxify_filter_pipeline.py b/examples/detoxify_filter_pipeline.py index 28f7138..7411b39 100644 --- a/examples/detoxify_filter_pipeline.py +++ b/examples/detoxify_filter_pipeline.py @@ -16,6 +16,17 @@ import os class Pipeline: + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + # e.g. ["llama3:latest", "gpt-3.5-turbo"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + def __init__(self): # Pipeline filters are only compatible with Open WebUI # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. @@ -28,19 +39,8 @@ class Pipeline: self.id = "detoxify_filter_pipeline" self.name = "Detoxify Filter" - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - # e.g. ["llama3:latest", "gpt-3.5-turbo"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - # Initialize - self.valves = Valves( + self.valves = self.Valves( **{ "pipelines": ["*"], # Connect to all pipelines } diff --git a/examples/example_pipeline.py b/examples/example_pipeline.py index 417974e..c321f4b 100644 --- a/examples/example_pipeline.py +++ b/examples/example_pipeline.py @@ -1,8 +1,12 @@ from typing import List, Union, Generator, Iterator from schemas import OpenAIChatMessage +from pydantic import BaseModel class Pipeline: + class Valves(BaseModel): + pass + def __init__(self): # Optionally, you can set the id and name of the pipeline. # Assign a unique identifier to the pipeline. diff --git a/examples/filter_pipeline.py b/examples/filter_pipeline.py index db69271..0e303c4 100644 --- a/examples/filter_pipeline.py +++ b/examples/filter_pipeline.py @@ -14,6 +14,19 @@ from schemas import OpenAIChatMessage class Pipeline: + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + + # Add your custom parameters here + pass + def __init__(self): # Pipeline filters are only compatible with Open WebUI # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. @@ -26,20 +39,7 @@ class Pipeline: self.id = "filter_pipeline" self.name = "Filter" - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - - # Add your custom parameters here - pass - - self.valves = Valves(**{"pipelines": ["llama3:latest"]}) + self.valves = self.Valves(**{"pipelines": ["llama3:latest"]}) pass diff --git a/examples/langfuse_filter_pipeline.py b/examples/langfuse_filter_pipeline.py index f985419..6dd4894 100644 --- a/examples/langfuse_filter_pipeline.py +++ b/examples/langfuse_filter_pipeline.py @@ -18,6 +18,23 @@ from langfuse import Langfuse class Pipeline: + + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + # e.g. ["llama3:latest", "gpt-3.5-turbo"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + + # Valves + secret_key: str + public_key: str + host: str + def __init__(self): # Pipeline filters are only compatible with Open WebUI # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. @@ -30,24 +47,8 @@ class Pipeline: self.id = "langfuse_filter_pipeline" self.name = "Langfuse Filter" - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - # e.g. ["llama3:latest", "gpt-3.5-turbo"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - - # Valves - secret_key: str - public_key: str - host: str - # Initialize - self.valves = Valves( + self.valves = self.Valves( **{ "pipelines": ["*"], # Connect to all pipelines "secret_key": os.getenv("LANGFUSE_SECRET_KEY"), @@ -94,7 +95,7 @@ class Pipeline: input=body, user_id=user["id"], metadata={"name": user["name"]}, - session_id=body["chat_id"] + session_id=body["chat_id"], ) print(trace.get_trace_url()) diff --git a/examples/libretranslate_filter_pipeline.py b/examples/libretranslate_filter_pipeline.py index 5a8745b..0c14ac8 100644 --- a/examples/libretranslate_filter_pipeline.py +++ b/examples/libretranslate_filter_pipeline.py @@ -8,6 +8,31 @@ from utils.main import get_last_user_message, get_last_assistant_message class Pipeline: + + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + # e.g. ["llama3:latest", "gpt-3.5-turbo"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + + # Valves + libretranslate_url: str + + # Source and target languages + # User message will be translated from source_user to target_user + source_user: Optional[str] = "auto" + target_user: Optional[str] = "en" + + # Assistant languages + # Assistant message will be translated from source_assistant to target_assistant + source_assistant: Optional[str] = "en" + target_assistant: Optional[str] = "es" + def __init__(self): # Pipeline filters are only compatible with Open WebUI # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. @@ -20,32 +45,8 @@ class Pipeline: self.id = "libretranslate_filter_pipeline" self.name = "LibreTranslate Filter" - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - # e.g. ["llama3:latest", "gpt-3.5-turbo"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - - # Valves - libretranslate_url: str - - # Source and target languages - # User message will be translated from source_user to target_user - source_user: Optional[str] = "auto" - target_user: Optional[str] = "en" - - # Assistant languages - # Assistant message will be translated from source_assistant to target_assistant - source_assistant: Optional[str] = "en" - target_assistant: Optional[str] = "es" - # Initialize - self.valves = Valves( + self.valves = self.Valves( **{ "pipelines": ["*"], # Connect to all pipelines "libretranslate_url": os.getenv( diff --git a/examples/litellm_manifold_pipeline.py b/examples/litellm_manifold_pipeline.py index a739730..f12e93a 100644 --- a/examples/litellm_manifold_pipeline.py +++ b/examples/litellm_manifold_pipeline.py @@ -14,6 +14,10 @@ import requests class Pipeline: + + class Valves(BaseModel): + LITELLM_BASE_URL: str + def __init__(self): # You can also set the pipelines that are available in this pipeline. # Set manifold to True if you want to use this pipeline as a manifold. @@ -29,11 +33,8 @@ class Pipeline: # Optionally, you can set the name of the manifold pipeline. self.name = "LiteLLM: " - class Valves(BaseModel): - LITELLM_BASE_URL: str - # Initialize rate limits - self.valves = Valves(**{"LITELLM_BASE_URL": "http://localhost:4001"}) + self.valves = self.Valves(**{"LITELLM_BASE_URL": "http://localhost:4001"}) self.pipelines = [] pass diff --git a/examples/litellm_subprocess_manifold_pipeline.py b/examples/litellm_subprocess_manifold_pipeline.py index e10ee87..f2bdeff 100644 --- a/examples/litellm_subprocess_manifold_pipeline.py +++ b/examples/litellm_subprocess_manifold_pipeline.py @@ -21,6 +21,12 @@ import yaml class Pipeline: + class Valves(BaseModel): + LITELLM_CONFIG_DIR: str = "./litellm/config.yaml" + LITELLM_PROXY_PORT: int = 4001 + LITELLM_PROXY_HOST: str = "127.0.0.1" + litellm_config: dict = {} + def __init__(self): # You can also set the pipelines that are available in this pipeline. # Set manifold to True if you want to use this pipeline as a manifold. @@ -36,14 +42,8 @@ class Pipeline: # Optionally, you can set the name of the manifold pipeline. self.name = "LiteLLM: " - class Valves(BaseModel): - LITELLM_CONFIG_DIR: str = "./litellm/config.yaml" - LITELLM_PROXY_PORT: int = 4001 - LITELLM_PROXY_HOST: str = "127.0.0.1" - litellm_config: dict = {} - # Initialize Valves - self.valves = Valves(**{"LITELLM_CONFIG_DIR": f"./litellm/config.yaml"}) + self.valves = self.Valves(**{"LITELLM_CONFIG_DIR": f"./litellm/config.yaml"}) self.background_process = None pass diff --git a/examples/ollama_manifold_pipeline.py b/examples/ollama_manifold_pipeline.py index 6fc18f8..a564113 100644 --- a/examples/ollama_manifold_pipeline.py +++ b/examples/ollama_manifold_pipeline.py @@ -5,6 +5,10 @@ import requests class Pipeline: + + class Valves(BaseModel): + OLLAMA_BASE_URL: str + def __init__(self): # You can also set the pipelines that are available in this pipeline. # Set manifold to True if you want to use this pipeline as a manifold. @@ -20,10 +24,7 @@ class Pipeline: # Optionally, you can set the name of the manifold pipeline. self.name = "Ollama: " - class Valves(BaseModel): - OLLAMA_BASE_URL: str - - self.valves = Valves(**{"OLLAMA_BASE_URL": "http://localhost:11435"}) + self.valves = self.Valves(**{"OLLAMA_BASE_URL": "http://localhost:11435"}) self.pipelines = [] pass diff --git a/examples/rate_limit_filter_pipeline.py b/examples/rate_limit_filter_pipeline.py index 49ae733..f03e18b 100644 --- a/examples/rate_limit_filter_pipeline.py +++ b/examples/rate_limit_filter_pipeline.py @@ -4,7 +4,24 @@ from pydantic import BaseModel from schemas import OpenAIChatMessage import time + class Pipeline: + class Valves(BaseModel): + # List target pipeline ids (models) that this filter will be connected to. + # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] + pipelines: List[str] = [] + + # Assign a priority level to the filter pipeline. + # The priority level determines the order in which the filter pipelines are executed. + # The lower the number, the higher the priority. + priority: int = 0 + + # Valves for rate limiting + requests_per_minute: Optional[int] = None + requests_per_hour: Optional[int] = None + sliding_window_limit: Optional[int] = None + sliding_window_minutes: Optional[int] = None + def __init__(self): # Pipeline filters are only compatible with Open WebUI # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. @@ -16,36 +33,22 @@ class Pipeline: self.id = "rate_limit_filter_pipeline" self.name = "Rate Limit Filter" - class Valves(BaseModel): - # List target pipeline ids (models) that this filter will be connected to. - # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] - pipelines: List[str] = [] - - # Assign a priority level to the filter pipeline. - # The priority level determines the order in which the filter pipelines are executed. - # The lower the number, the higher the priority. - priority: int = 0 - - # Valves for rate limiting - requests_per_minute: Optional[int] = None - requests_per_hour: Optional[int] = None - sliding_window_limit: Optional[int] = None - sliding_window_minutes: Optional[int] = None - # Initialize rate limits - pipelines = os.getenv("RATE_LIMIT_PIPELINES", "*").split(",") - requests_per_minute = int(os.getenv("RATE_LIMIT_REQUESTS_PER_MINUTE", 10)) - requests_per_hour = int(os.getenv("RATE_LIMIT_REQUESTS_PER_HOUR", 1000)) - sliding_window_limit = int(os.getenv("RATE_LIMIT_SLIDING_WINDOW_LIMIT", 100)) - sliding_window_minutes = int(os.getenv("RATE_LIMIT_SLIDING_WINDOW_MINUTES", 15)) - - self.valves = Valves( + self.valves = self.Valves( **{ - "pipelines": pipelines, - "requests_per_minute": requests_per_minute, - "requests_per_hour": requests_per_hour, - "sliding_window_limit": sliding_window_limit, - "sliding_window_minutes": sliding_window_minutes, + "pipelines": os.getenv("RATE_LIMIT_PIPELINES", "*").split(","), + "requests_per_minute": int( + os.getenv("RATE_LIMIT_REQUESTS_PER_MINUTE", 10) + ), + "requests_per_hour": int( + os.getenv("RATE_LIMIT_REQUESTS_PER_HOUR", 1000) + ), + "sliding_window_limit": int( + os.getenv("RATE_LIMIT_SLIDING_WINDOW_LIMIT", 100) + ), + "sliding_window_minutes": int( + os.getenv("RATE_LIMIT_SLIDING_WINDOW_MINUTES", 15) + ), } ) diff --git a/examples/wikipedia_pipeline.py b/examples/wikipedia_pipeline.py index 1a296ae..d430123 100644 --- a/examples/wikipedia_pipeline.py +++ b/examples/wikipedia_pipeline.py @@ -6,6 +6,9 @@ import os class Pipeline: + class Valves(BaseModel): + pass + def __init__(self): # Assign a unique identifier to the pipeline. # The identifier must be unique across all pipelines. @@ -13,11 +16,8 @@ class Pipeline: self.id = "wiki_pipeline" self.name = "Wikipedia Pipeline" - class Valves(BaseModel): - pass - # Initialize rate limits - self.valves = Valves(**{"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", "")}) + self.valves = self.Valves(**{"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", "")}) async def on_startup(self): # This function is called when the server is started. From eb8ff0d12d6c4c677f8eed63db918845de37024c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 11:38:49 -0700 Subject: [PATCH 04/51] chore: comments --- examples/function_calling_scaffold.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/function_calling_scaffold.py b/examples/function_calling_scaffold.py index aae6e50..387d49f 100644 --- a/examples/function_calling_scaffold.py +++ b/examples/function_calling_scaffold.py @@ -10,9 +10,8 @@ class Pipeline(FunctionCallingBlueprint): def __init__(self, pipeline) -> None: self.pipeline = pipeline - # Add your custom tools here + # Add your custom tools using pure Python code here, make sure to add type hints # Please refer to function_calling_filter_pipeline.py for an example - # Pure Python code can be added here pass def __init__(self): From 8aa82f9eb96eedf1ec2ac505046fc7ee455ffcbb Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 11:45:29 -0700 Subject: [PATCH 05/51] chore --- data/superlinear.txt | 171 ------------------ .../{ => automation}/applescript_pipeline.py | 0 .../{ => automation}/python_code_pipeline.py | 0 .../{ => automation}/wikipedia_pipeline.py | 0 .../conversation_turn_limit_filter.py | 0 .../{ => filters}/detoxify_filter_pipeline.py | 0 .../{ => filters}/langfuse_filter_pipeline.py | 0 .../libretranslate_filter_pipeline.py | 0 .../rate_limit_filter_pipeline.py | 0 .../function_calling_filter_pipeline.py | 0 .../anthropic_manifold_pipeline.py | 0 .../{ => providers}/azure_openai_pipeline.py | 0 .../cohere_manifold_pipeline.py | 0 .../litellm_manifold_pipeline.py | 0 .../litellm_subprocess_manifold_pipeline.py | 0 .../{ => providers}/llama_cpp_pipeline.py | 0 examples/{ => providers}/mlx_pipeline.py | 0 .../ollama_manifold_pipeline.py | 0 examples/{ => providers}/ollama_pipeline.py | 0 examples/{ => providers}/openai_pipeline.py | 0 examples/{ => rag}/haystack_pipeline.py | 0 .../llamaindex_ollama_github_pipeline.py | 0 .../{ => rag}/llamaindex_ollama_pipeline.py | 0 examples/{ => rag}/llamaindex_pipeline.py | 0 .../example_pipeline_scaffold.py} | 0 .../filter_pipeline_scaffold.py} | 0 .../function_calling_scaffold.py | 0 .../manifold_pipeline_scaffold.py} | 0 28 files changed, 171 deletions(-) delete mode 100644 data/superlinear.txt rename examples/{ => automation}/applescript_pipeline.py (100%) rename examples/{ => automation}/python_code_pipeline.py (100%) rename examples/{ => automation}/wikipedia_pipeline.py (100%) rename examples/{ => filters}/conversation_turn_limit_filter.py (100%) rename examples/{ => filters}/detoxify_filter_pipeline.py (100%) rename examples/{ => filters}/langfuse_filter_pipeline.py (100%) rename examples/{ => filters}/libretranslate_filter_pipeline.py (100%) rename examples/{ => filters}/rate_limit_filter_pipeline.py (100%) rename examples/{ => function_calling}/function_calling_filter_pipeline.py (100%) rename examples/{ => providers}/anthropic_manifold_pipeline.py (100%) rename examples/{ => providers}/azure_openai_pipeline.py (100%) rename examples/{ => providers}/cohere_manifold_pipeline.py (100%) rename examples/{ => providers}/litellm_manifold_pipeline.py (100%) rename examples/{ => providers}/litellm_subprocess_manifold_pipeline.py (100%) rename examples/{ => providers}/llama_cpp_pipeline.py (100%) rename examples/{ => providers}/mlx_pipeline.py (100%) rename examples/{ => providers}/ollama_manifold_pipeline.py (100%) rename examples/{ => providers}/ollama_pipeline.py (100%) rename examples/{ => providers}/openai_pipeline.py (100%) rename examples/{ => rag}/haystack_pipeline.py (100%) rename examples/{ => rag}/llamaindex_ollama_github_pipeline.py (100%) rename examples/{ => rag}/llamaindex_ollama_pipeline.py (100%) rename examples/{ => rag}/llamaindex_pipeline.py (100%) rename examples/{example_pipeline.py => scaffolds/example_pipeline_scaffold.py} (100%) rename examples/{filter_pipeline.py => scaffolds/filter_pipeline_scaffold.py} (100%) rename examples/{ => scaffolds}/function_calling_scaffold.py (100%) rename examples/{manifold_pipeline.py => scaffolds/manifold_pipeline_scaffold.py} (100%) diff --git a/data/superlinear.txt b/data/superlinear.txt deleted file mode 100644 index 14f3f83..0000000 --- a/data/superlinear.txt +++ /dev/null @@ -1,171 +0,0 @@ -October 2023 - -One of the most important things I didn't understand about the world when I was a child is the degree to which the returns for performance are superlinear. - -Teachers and coaches implicitly told us the returns were linear. "You get out," I heard a thousand times, "what you put in." They meant well, but this is rarely true. If your product is only half as good as your competitor's, you don't get half as many customers. You get no customers, and you go out of business. - -It's obviously true that the returns for performance are superlinear in business. Some think this is a flaw of capitalism, and that if we changed the rules it would stop being true. But superlinear returns for performance are a feature of the world, not an artifact of rules we've invented. We see the same pattern in fame, power, military victories, knowledge, and even benefit to humanity. In all of these, the rich get richer. [1] - -You can't understand the world without understanding the concept of superlinear returns. And if you're ambitious you definitely should, because this will be the wave you surf on. - - - - - -It may seem as if there are a lot of different situations with superlinear returns, but as far as I can tell they reduce to two fundamental causes: exponential growth and thresholds. - -The most obvious case of superlinear returns is when you're working on something that grows exponentially. For example, growing bacterial cultures. When they grow at all, they grow exponentially. But they're tricky to grow. Which means the difference in outcome between someone who's adept at it and someone who's not is very great. - -Startups can also grow exponentially, and we see the same pattern there. Some manage to achieve high growth rates. Most don't. And as a result you get qualitatively different outcomes: the companies with high growth rates tend to become immensely valuable, while the ones with lower growth rates may not even survive. - -Y Combinator encourages founders to focus on growth rate rather than absolute numbers. It prevents them from being discouraged early on, when the absolute numbers are still low. It also helps them decide what to focus on: you can use growth rate as a compass to tell you how to evolve the company. But the main advantage is that by focusing on growth rate you tend to get something that grows exponentially. - -YC doesn't explicitly tell founders that with growth rate "you get out what you put in," but it's not far from the truth. And if growth rate were proportional to performance, then the reward for performance p over time t would be proportional to pt. - -Even after decades of thinking about this, I find that sentence startling. - -Whenever how well you do depends on how well you've done, you'll get exponential growth. But neither our DNA nor our customs prepare us for it. No one finds exponential growth natural; every child is surprised, the first time they hear it, by the story of the man who asks the king for a single grain of rice the first day and double the amount each successive day. - -What we don't understand naturally we develop customs to deal with, but we don't have many customs about exponential growth either, because there have been so few instances of it in human history. In principle herding should have been one: the more animals you had, the more offspring they'd have. But in practice grazing land was the limiting factor, and there was no plan for growing that exponentially. - -Or more precisely, no generally applicable plan. There was a way to grow one's territory exponentially: by conquest. The more territory you control, the more powerful your army becomes, and the easier it is to conquer new territory. This is why history is full of empires. But so few people created or ran empires that their experiences didn't affect customs very much. The emperor was a remote and terrifying figure, not a source of lessons one could use in one's own life. - -The most common case of exponential growth in preindustrial times was probably scholarship. The more you know, the easier it is to learn new things. The result, then as now, was that some people were startlingly more knowledgeable than the rest about certain topics. But this didn't affect customs much either. Although empires of ideas can overlap and there can thus be far more emperors, in preindustrial times this type of empire had little practical effect. [2] - -That has changed in the last few centuries. Now the emperors of ideas can design bombs that defeat the emperors of territory. But this phenomenon is still so new that we haven't fully assimilated it. Few even of the participants realize they're benefitting from exponential growth or ask what they can learn from other instances of it. - -The other source of superlinear returns is embodied in the expression "winner take all." In a sports match the relationship between performance and return is a step function: the winning team gets one win whether they do much better or just slightly better. [3] - -The source of the step function is not competition per se, however. It's that there are thresholds in the outcome. You don't need competition to get those. There can be thresholds in situations where you're the only participant, like proving a theorem or hitting a target. - -It's remarkable how often a situation with one source of superlinear returns also has the other. Crossing thresholds leads to exponential growth: the winning side in a battle usually suffers less damage, which makes them more likely to win in the future. And exponential growth helps you cross thresholds: in a market with network effects, a company that grows fast enough can shut out potential competitors. - -Fame is an interesting example of a phenomenon that combines both sources of superlinear returns. Fame grows exponentially because existing fans bring you new ones. But the fundamental reason it's so concentrated is thresholds: there's only so much room on the A-list in the average person's head. - -The most important case combining both sources of superlinear returns may be learning. Knowledge grows exponentially, but there are also thresholds in it. Learning to ride a bicycle, for example. Some of these thresholds are akin to machine tools: once you learn to read, you're able to learn anything else much faster. But the most important thresholds of all are those representing new discoveries. Knowledge seems to be fractal in the sense that if you push hard at the boundary of one area of knowledge, you sometimes discover a whole new field. And if you do, you get first crack at all the new discoveries to be made in it. Newton did this, and so did Durer and Darwin. - - - - - -Are there general rules for finding situations with superlinear returns? The most obvious one is to seek work that compounds. - -There are two ways work can compound. It can compound directly, in the sense that doing well in one cycle causes you to do better in the next. That happens for example when you're building infrastructure, or growing an audience or brand. Or work can compound by teaching you, since learning compounds. This second case is an interesting one because you may feel you're doing badly as it's happening. You may be failing to achieve your immediate goal. But if you're learning a lot, then you're getting exponential growth nonetheless. - -This is one reason Silicon Valley is so tolerant of failure. People in Silicon Valley aren't blindly tolerant of failure. They'll only continue to bet on you if you're learning from your failures. But if you are, you are in fact a good bet: maybe your company didn't grow the way you wanted, but you yourself have, and that should yield results eventually. - -Indeed, the forms of exponential growth that don't consist of learning are so often intermixed with it that we should probably treat this as the rule rather than the exception. Which yields another heuristic: always be learning. If you're not learning, you're probably not on a path that leads to superlinear returns. - -But don't overoptimize what you're learning. Don't limit yourself to learning things that are already known to be valuable. You're learning; you don't know for sure yet what's going to be valuable, and if you're too strict you'll lop off the outliers. - -What about step functions? Are there also useful heuristics of the form "seek thresholds" or "seek competition?" Here the situation is trickier. The existence of a threshold doesn't guarantee the game will be worth playing. If you play a round of Russian roulette, you'll be in a situation with a threshold, certainly, but in the best case you're no better off. "Seek competition" is similarly useless; what if the prize isn't worth competing for? Sufficiently fast exponential growth guarantees both the shape and magnitude of the return curve — because something that grows fast enough will grow big even if it's trivially small at first — but thresholds only guarantee the shape. [4] - -A principle for taking advantage of thresholds has to include a test to ensure the game is worth playing. Here's one that does: if you come across something that's mediocre yet still popular, it could be a good idea to replace it. For example, if a company makes a product that people dislike yet still buy, then presumably they'd buy a better alternative if you made one. [5] - -It would be great if there were a way to find promising intellectual thresholds. Is there a way to tell which questions have whole new fields beyond them? I doubt we could ever predict this with certainty, but the prize is so valuable that it would be useful to have predictors that were even a little better than random, and there's hope of finding those. We can to some degree predict when a research problem isn't likely to lead to new discoveries: when it seems legit but boring. Whereas the kind that do lead to new discoveries tend to seem very mystifying, but perhaps unimportant. (If they were mystifying and obviously important, they'd be famous open questions with lots of people already working on them.) So one heuristic here is to be driven by curiosity rather than careerism — to give free rein to your curiosity instead of working on what you're supposed to. - - - - - -The prospect of superlinear returns for performance is an exciting one for the ambitious. And there's good news in this department: this territory is expanding in both directions. There are more types of work in which you can get superlinear returns, and the returns themselves are growing. - -There are two reasons for this, though they're so closely intertwined that they're more like one and a half: progress in technology, and the decreasing importance of organizations. - -Fifty years ago it used to be much more necessary to be part of an organization to work on ambitious projects. It was the only way to get the resources you needed, the only way to have colleagues, and the only way to get distribution. So in 1970 your prestige was in most cases the prestige of the organization you belonged to. And prestige was an accurate predictor, because if you weren't part of an organization, you weren't likely to achieve much. There were a handful of exceptions, most notably artists and writers, who worked alone using inexpensive tools and had their own brands. But even they were at the mercy of organizations for reaching audiences. [6] - -A world dominated by organizations damped variation in the returns for performance. But this world has eroded significantly just in my lifetime. Now a lot more people can have the freedom that artists and writers had in the 20th century. There are lots of ambitious projects that don't require much initial funding, and lots of new ways to learn, make money, find colleagues, and reach audiences. - -There's still plenty of the old world left, but the rate of change has been dramatic by historical standards. Especially considering what's at stake. It's hard to imagine a more fundamental change than one in the returns for performance. - -Without the damping effect of institutions, there will be more variation in outcomes. Which doesn't imply everyone will be better off: people who do well will do even better, but those who do badly will do worse. That's an important point to bear in mind. Exposing oneself to superlinear returns is not for everyone. Most people will be better off as part of the pool. So who should shoot for superlinear returns? Ambitious people of two types: those who know they're so good that they'll be net ahead in a world with higher variation, and those, particularly the young, who can afford to risk trying it to find out. [7] - -The switch away from institutions won't simply be an exodus of their current inhabitants. Many of the new winners will be people they'd never have let in. So the resulting democratization of opportunity will be both greater and more authentic than any tame intramural version the institutions themselves might have cooked up. - - - - - -Not everyone is happy about this great unlocking of ambition. It threatens some vested interests and contradicts some ideologies. [8] But if you're an ambitious individual it's good news for you. How should you take advantage of it? - -The most obvious way to take advantage of superlinear returns for performance is by doing exceptionally good work. At the far end of the curve, incremental effort is a bargain. All the more so because there's less competition at the far end — and not just for the obvious reason that it's hard to do something exceptionally well, but also because people find the prospect so intimidating that few even try. Which means it's not just a bargain to do exceptional work, but a bargain even to try to. - -There are many variables that affect how good your work is, and if you want to be an outlier you need to get nearly all of them right. For example, to do something exceptionally well, you have to be interested in it. Mere diligence is not enough. So in a world with superlinear returns, it's even more valuable to know what you're interested in, and to find ways to work on it. [9] It will also be important to choose work that suits your circumstances. For example, if there's a kind of work that inherently requires a huge expenditure of time and energy, it will be increasingly valuable to do it when you're young and don't yet have children. - -There's a surprising amount of technique to doing great work. It's not just a matter of trying hard. I'm going to take a shot giving a recipe in one paragraph. - -Choose work you have a natural aptitude for and a deep interest in. Develop a habit of working on your own projects; it doesn't matter what they are so long as you find them excitingly ambitious. Work as hard as you can without burning out, and this will eventually bring you to one of the frontiers of knowledge. These look smooth from a distance, but up close they're full of gaps. Notice and explore such gaps, and if you're lucky one will expand into a whole new field. Take as much risk as you can afford; if you're not failing occasionally you're probably being too conservative. Seek out the best colleagues. Develop good taste and learn from the best examples. Be honest, especially with yourself. Exercise and eat and sleep well and avoid the more dangerous drugs. When in doubt, follow your curiosity. It never lies, and it knows more than you do about what's worth paying attention to. [10] - -And there is of course one other thing you need: to be lucky. Luck is always a factor, but it's even more of a factor when you're working on your own rather than as part of an organization. And though there are some valid aphorisms about luck being where preparedness meets opportunity and so on, there's also a component of true chance that you can't do anything about. The solution is to take multiple shots. Which is another reason to start taking risks early. - - - - - -The best example of a field with superlinear returns is probably science. It has exponential growth, in the form of learning, combined with thresholds at the extreme edge of performance — literally at the limits of knowledge. - -The result has been a level of inequality in scientific discovery that makes the wealth inequality of even the most stratified societies seem mild by comparison. Newton's discoveries were arguably greater than all his contemporaries' combined. [11] - -This point may seem obvious, but it might be just as well to spell it out. Superlinear returns imply inequality. The steeper the return curve, the greater the variation in outcomes. - -In fact, the correlation between superlinear returns and inequality is so strong that it yields another heuristic for finding work of this type: look for fields where a few big winners outperform everyone else. A kind of work where everyone does about the same is unlikely to be one with superlinear returns. - -What are fields where a few big winners outperform everyone else? Here are some obvious ones: sports, politics, art, music, acting, directing, writing, math, science, starting companies, and investing. In sports the phenomenon is due to externally imposed thresholds; you only need to be a few percent faster to win every race. In politics, power grows much as it did in the days of emperors. And in some of the other fields (including politics) success is driven largely by fame, which has its own source of superlinear growth. But when we exclude sports and politics and the effects of fame, a remarkable pattern emerges: the remaining list is exactly the same as the list of fields where you have to be independent-minded to succeed — where your ideas have to be not just correct, but novel as well. [12] - -This is obviously the case in science. You can't publish papers saying things that other people have already said. But it's just as true in investing, for example. It's only useful to believe that a company will do well if most other investors don't; if everyone else thinks the company will do well, then its stock price will already reflect that, and there's no room to make money. - -What else can we learn from these fields? In all of them you have to put in the initial effort. Superlinear returns seem small at first. At this rate, you find yourself thinking, I'll never get anywhere. But because the reward curve rises so steeply at the far end, it's worth taking extraordinary measures to get there. - -In the startup world, the name for this principle is "do things that don't scale." If you pay a ridiculous amount of attention to your tiny initial set of customers, ideally you'll kick off exponential growth by word of mouth. But this same principle applies to anything that grows exponentially. Learning, for example. When you first start learning something, you feel lost. But it's worth making the initial effort to get a toehold, because the more you learn, the easier it will get. - -There's another more subtle lesson in the list of fields with superlinear returns: not to equate work with a job. For most of the 20th century the two were identical for nearly everyone, and as a result we've inherited a custom that equates productivity with having a job. Even now to most people the phrase "your work" means their job. But to a writer or artist or scientist it means whatever they're currently studying or creating. For someone like that, their work is something they carry with them from job to job, if they have jobs at all. It may be done for an employer, but it's part of their portfolio. - - - - - -It's an intimidating prospect to enter a field where a few big winners outperform everyone else. Some people do this deliberately, but you don't need to. If you have sufficient natural ability and you follow your curiosity sufficiently far, you'll end up in one. Your curiosity won't let you be interested in boring questions, and interesting questions tend to create fields with superlinear returns if they're not already part of one. - -The territory of superlinear returns is by no means static. Indeed, the most extreme returns come from expanding it. So while both ambition and curiosity can get you into this territory, curiosity may be the more powerful of the two. Ambition tends to make you climb existing peaks, but if you stick close enough to an interesting enough question, it may grow into a mountain beneath you. - - - - - - - - - -Notes - -There's a limit to how sharply you can distinguish between effort, performance, and return, because they're not sharply distinguished in fact. What counts as return to one person might be performance to another. But though the borders of these concepts are blurry, they're not meaningless. I've tried to write about them as precisely as I could without crossing into error. - -[1] Evolution itself is probably the most pervasive example of superlinear returns for performance. But this is hard for us to empathize with because we're not the recipients; we're the returns. - -[2] Knowledge did of course have a practical effect before the Industrial Revolution. The development of agriculture changed human life completely. But this kind of change was the result of broad, gradual improvements in technique, not the discoveries of a few exceptionally learned people. - -[3] It's not mathematically correct to describe a step function as superlinear, but a step function starting from zero works like a superlinear function when it describes the reward curve for effort by a rational actor. If it starts at zero then the part before the step is below any linearly increasing return, and the part after the step must be above the necessary return at that point or no one would bother. - -[4] Seeking competition could be a good heuristic in the sense that some people find it motivating. It's also somewhat of a guide to promising problems, because it's a sign that other people find them promising. But it's a very imperfect sign: often there's a clamoring crowd chasing some problem, and they all end up being trumped by someone quietly working on another one. - -[5] Not always, though. You have to be careful with this rule. When something is popular despite being mediocre, there's often a hidden reason why. Perhaps monopoly or regulation make it hard to compete. Perhaps customers have bad taste or have broken procedures for deciding what to buy. There are huge swathes of mediocre things that exist for such reasons. - -[6] In my twenties I wanted to be an artist and even went to art school to study painting. Mostly because I liked art, but a nontrivial part of my motivation came from the fact that artists seemed least at the mercy of organizations. - -[7] In principle everyone is getting superlinear returns. Learning compounds, and everyone learns in the course of their life. But in practice few push this kind of everyday learning to the point where the return curve gets really steep. - -[8] It's unclear exactly what advocates of "equity" mean by it. They seem to disagree among themselves. But whatever they mean is probably at odds with a world in which institutions have less power to control outcomes, and a handful of outliers do much better than everyone else. - -It may seem like bad luck for this concept that it arose at just the moment when the world was shifting in the opposite direction, but I don't think this was a coincidence. I think one reason it arose now is because its adherents feel threatened by rapidly increasing variation in performance. - -[9] Corollary: Parents who pressure their kids to work on something prestigious, like medicine, even though they have no interest in it, will be hosing them even more than they have in the past. - -[10] The original version of this paragraph was the first draft of "How to Do Great Work." As soon as I wrote it I realized it was a more important topic than superlinear returns, so I paused the present essay to expand this paragraph into its own. Practically nothing remains of the original version, because after I finished "How to Do Great Work" I rewrote it based on that. - -[11] Before the Industrial Revolution, people who got rich usually did it like emperors: capturing some resource made them more powerful and enabled them to capture more. Now it can be done like a scientist, by discovering or building something uniquely valuable. Most people who get rich use a mix of the old and the new ways, but in the most advanced economies the ratio has shifted dramatically toward discovery just in the last half century. - -[12] It's not surprising that conventional-minded people would dislike inequality if independent-mindedness is one of the biggest drivers of it. But it's not simply that they don't want anyone to have what they can't. The conventional-minded literally can't imagine what it's like to have novel ideas. So the whole phenomenon of great variation in performance seems unnatural to them, and when they encounter it they assume it must be due to cheating or to some malign external influence. - - - -Thanks to Trevor Blackwell, Patrick Collison, Tyler Cowen, Jessica Livingston, Harj Taggar, and Garry Tan for reading drafts of this. \ No newline at end of file diff --git a/examples/applescript_pipeline.py b/examples/automation/applescript_pipeline.py similarity index 100% rename from examples/applescript_pipeline.py rename to examples/automation/applescript_pipeline.py diff --git a/examples/python_code_pipeline.py b/examples/automation/python_code_pipeline.py similarity index 100% rename from examples/python_code_pipeline.py rename to examples/automation/python_code_pipeline.py diff --git a/examples/wikipedia_pipeline.py b/examples/automation/wikipedia_pipeline.py similarity index 100% rename from examples/wikipedia_pipeline.py rename to examples/automation/wikipedia_pipeline.py diff --git a/examples/conversation_turn_limit_filter.py b/examples/filters/conversation_turn_limit_filter.py similarity index 100% rename from examples/conversation_turn_limit_filter.py rename to examples/filters/conversation_turn_limit_filter.py diff --git a/examples/detoxify_filter_pipeline.py b/examples/filters/detoxify_filter_pipeline.py similarity index 100% rename from examples/detoxify_filter_pipeline.py rename to examples/filters/detoxify_filter_pipeline.py diff --git a/examples/langfuse_filter_pipeline.py b/examples/filters/langfuse_filter_pipeline.py similarity index 100% rename from examples/langfuse_filter_pipeline.py rename to examples/filters/langfuse_filter_pipeline.py diff --git a/examples/libretranslate_filter_pipeline.py b/examples/filters/libretranslate_filter_pipeline.py similarity index 100% rename from examples/libretranslate_filter_pipeline.py rename to examples/filters/libretranslate_filter_pipeline.py diff --git a/examples/rate_limit_filter_pipeline.py b/examples/filters/rate_limit_filter_pipeline.py similarity index 100% rename from examples/rate_limit_filter_pipeline.py rename to examples/filters/rate_limit_filter_pipeline.py diff --git a/examples/function_calling_filter_pipeline.py b/examples/function_calling/function_calling_filter_pipeline.py similarity index 100% rename from examples/function_calling_filter_pipeline.py rename to examples/function_calling/function_calling_filter_pipeline.py diff --git a/examples/anthropic_manifold_pipeline.py b/examples/providers/anthropic_manifold_pipeline.py similarity index 100% rename from examples/anthropic_manifold_pipeline.py rename to examples/providers/anthropic_manifold_pipeline.py diff --git a/examples/azure_openai_pipeline.py b/examples/providers/azure_openai_pipeline.py similarity index 100% rename from examples/azure_openai_pipeline.py rename to examples/providers/azure_openai_pipeline.py diff --git a/examples/cohere_manifold_pipeline.py b/examples/providers/cohere_manifold_pipeline.py similarity index 100% rename from examples/cohere_manifold_pipeline.py rename to examples/providers/cohere_manifold_pipeline.py diff --git a/examples/litellm_manifold_pipeline.py b/examples/providers/litellm_manifold_pipeline.py similarity index 100% rename from examples/litellm_manifold_pipeline.py rename to examples/providers/litellm_manifold_pipeline.py diff --git a/examples/litellm_subprocess_manifold_pipeline.py b/examples/providers/litellm_subprocess_manifold_pipeline.py similarity index 100% rename from examples/litellm_subprocess_manifold_pipeline.py rename to examples/providers/litellm_subprocess_manifold_pipeline.py diff --git a/examples/llama_cpp_pipeline.py b/examples/providers/llama_cpp_pipeline.py similarity index 100% rename from examples/llama_cpp_pipeline.py rename to examples/providers/llama_cpp_pipeline.py diff --git a/examples/mlx_pipeline.py b/examples/providers/mlx_pipeline.py similarity index 100% rename from examples/mlx_pipeline.py rename to examples/providers/mlx_pipeline.py diff --git a/examples/ollama_manifold_pipeline.py b/examples/providers/ollama_manifold_pipeline.py similarity index 100% rename from examples/ollama_manifold_pipeline.py rename to examples/providers/ollama_manifold_pipeline.py diff --git a/examples/ollama_pipeline.py b/examples/providers/ollama_pipeline.py similarity index 100% rename from examples/ollama_pipeline.py rename to examples/providers/ollama_pipeline.py diff --git a/examples/openai_pipeline.py b/examples/providers/openai_pipeline.py similarity index 100% rename from examples/openai_pipeline.py rename to examples/providers/openai_pipeline.py diff --git a/examples/haystack_pipeline.py b/examples/rag/haystack_pipeline.py similarity index 100% rename from examples/haystack_pipeline.py rename to examples/rag/haystack_pipeline.py diff --git a/examples/llamaindex_ollama_github_pipeline.py b/examples/rag/llamaindex_ollama_github_pipeline.py similarity index 100% rename from examples/llamaindex_ollama_github_pipeline.py rename to examples/rag/llamaindex_ollama_github_pipeline.py diff --git a/examples/llamaindex_ollama_pipeline.py b/examples/rag/llamaindex_ollama_pipeline.py similarity index 100% rename from examples/llamaindex_ollama_pipeline.py rename to examples/rag/llamaindex_ollama_pipeline.py diff --git a/examples/llamaindex_pipeline.py b/examples/rag/llamaindex_pipeline.py similarity index 100% rename from examples/llamaindex_pipeline.py rename to examples/rag/llamaindex_pipeline.py diff --git a/examples/example_pipeline.py b/examples/scaffolds/example_pipeline_scaffold.py similarity index 100% rename from examples/example_pipeline.py rename to examples/scaffolds/example_pipeline_scaffold.py diff --git a/examples/filter_pipeline.py b/examples/scaffolds/filter_pipeline_scaffold.py similarity index 100% rename from examples/filter_pipeline.py rename to examples/scaffolds/filter_pipeline_scaffold.py diff --git a/examples/function_calling_scaffold.py b/examples/scaffolds/function_calling_scaffold.py similarity index 100% rename from examples/function_calling_scaffold.py rename to examples/scaffolds/function_calling_scaffold.py diff --git a/examples/manifold_pipeline.py b/examples/scaffolds/manifold_pipeline_scaffold.py similarity index 100% rename from examples/manifold_pipeline.py rename to examples/scaffolds/manifold_pipeline_scaffold.py From 3d758a9c9458631e99606169e24df777780a016e Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 11:58:59 -0700 Subject: [PATCH 06/51] feat: persistent valves --- main.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/main.py b/main.py index ebd41c7..dd3381f 100644 --- a/main.py +++ b/main.py @@ -124,8 +124,37 @@ async def load_modules_from_directory(directory): if filename.endswith(".py"): module_name = filename[:-3] # Remove the .py extension module_path = os.path.join(directory, filename) + + # Create subfolder matching the filename without the .py extension + subfolder_path = os.path.join(directory, module_name) + if not os.path.exists(subfolder_path): + os.makedirs(subfolder_path) + logging.info(f"Created subfolder: {subfolder_path}") + + # Create a valves.json file if it doesn't exist + valves_json_path = os.path.join(subfolder_path, "valves.json") + if not os.path.exists(valves_json_path): + with open(valves_json_path, "w") as f: + json.dump({}, f) + logging.info(f"Created valves.json in: {subfolder_path}") + pipeline = await load_module_from_path(module_name, module_path) if pipeline: + # Overwrite pipeline.valves with values from valves.json + if os.path.exists(valves_json_path): + with open(valves_json_path, "r") as f: + valves_json = json.load(f) + ValvesModel = pipeline.valves.__class__ + # Create a ValvesModel instance using default values and overwrite with valves_json + combined_valves = { + **pipeline.valves.model_dump(), + **valves_json, + } + valves = ValvesModel(**combined_valves) + pipeline.valves = valves + + logging.info(f"Updated valves for module: {module_name}") + pipeline_id = pipeline.id if hasattr(pipeline, "id") else module_name PIPELINE_MODULES[pipeline_id] = pipeline PIPELINE_NAMES[pipeline_id] = module_name @@ -441,6 +470,14 @@ async def update_valves(pipeline_id: str, form_data: dict): valves = ValvesModel(**form_data) pipeline.valves = valves + # Determine the directory path for the valves.json file + subfolder_path = os.path.join(PIPELINES_DIR, PIPELINE_NAMES[pipeline_id]) + valves_json_path = os.path.join(subfolder_path, "valves.json") + + # Save the updated valves data back to the valves.json file + with open(valves_json_path, "w") as f: + json.dump(valves.model_dump(), f) + if hasattr(pipeline, "on_valves_updated"): await pipeline.on_valves_updated() except Exception as e: From 585e2a6e8b61f5e242de347379977dfc07cbad6c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 13:42:55 -0700 Subject: [PATCH 07/51] doc: readme --- README.md | 47 ++++++++++++++++++++++++++------------- docs/images/workflow.png | Bin 60846 -> 57523 bytes 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ee5d976..8e2c638 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,39 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat Integrating Pipelines with any OpenAI API-compatible UI client is simple. Launch your Pipelines instance and set the OpenAI URL on your client to the Pipelines URL. That's it! You're ready to leverage any Python library for your needs. -## 📂 Directory Structure and Examples +## ⚡ Quick Start with Docker -The `/pipelines` directory is the core of your setup. Add new modules, customize existing ones, and manage your workflows here. All the pipelines in the `/pipelines` directory will be **automatically loaded** when the server launches. +For a streamlined setup using Docker: -### Integration Examples +1. **Run the Pipelines container:** -Find various integration examples in the `/pipelines/examples` directory. These examples show how to integrate different functionalities, providing a foundation for building your own custom pipelines. + ```sh + docker run -d -p 9099:9099 -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main + ``` + +2. **Connect to Open WebUI:** + + - Navigate to the **Settings > Connections > OpenAI API** section in Open WebUI. + - Set the API URL to `http://localhost:9099` and the API key to `0p3n-w3bu!`. Your filter should now be active. + +3. **Manage Configurations:** + + - In the admin panel, go to **Admin Settings > Pipelines tab**. + - Select your desired filter and modify the valve values directly from the WebUI. + +If you need to install a custom pipeline with additional dependencies: + +- **Run the following command:** + + ```sh + docker run -d -p 9099:9099 -e PIPELINES_PATH="https://github.com/open-webui/pipelines/blob/main/examples/filters/detoxify_filter_pipeline.py" -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main + ``` + +That's it! You're now ready to build customizable AI integrations effortlessly with Pipelines. Enjoy! ## 📦 Installation and Setup -Get started with Pipelines in a few steps: +Get started with Pipelines in a few easy steps: 1. **Ensure Python 3.11 is installed.** 2. **Clone the Pipelines repository:** @@ -54,20 +76,13 @@ Get started with Pipelines in a few steps: Once the server is running, set the OpenAI URL on your client to the Pipelines URL. This unlocks the full capabilities of Pipelines, integrating any Python library and creating custom workflows tailored to your needs. -## ⚡ Quick Start Example +## 📂 Directory Structure and Examples -1. **Copy and paste `rate_limit_filter_pipeline.py` from `/pipelines/examples` to the `/pipelines` folder.** -2. **Run the Pipelines server:** It will be hosted at `http://localhost:9099` by default. -3. **Configure Open WebUI:** +The `/pipelines` directory is the core of your setup. Add new modules, customize existing ones, and manage your workflows here. All the pipelines in the `/pipelines` directory will be **automatically loaded** when the server launches. - - In Open WebUI, go to **Settings > Connections > OpenAI API**. - - Set the API URL to `http://localhost:9099` and set the API key (default: `0p3n-w3bu!`). Your filter should now be active. +### Integration Examples -4. **Adjust Settings:** - - In the admin panel, go to **Admin Settings > Pipelines tab**. - - Select the filter and change the valve values directly from the WebUI. - -That's it! You're now set up with Pipelines. Enjoy building customizable AI integrations effortlessly! +Find various integration examples in the `/pipelines/examples` directory. These examples show how to integrate different functionalities, providing a foundation for building your own custom pipelines. ## 🎉 Work in Progress diff --git a/docs/images/workflow.png b/docs/images/workflow.png index 2fe23476f26d6f424b22a8998ce232a10ae9bc0c..8b6e9c8d65114f22facb71ae69d93ae945c3f70a 100644 GIT binary patch literal 57523 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 literal 60846 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 KcdEg3*#7{GoMPbs From 902c2ae7d6515a6246d787556c78d23dddb97f3a Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 13:50:05 -0700 Subject: [PATCH 08/51] doc: readme --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8e2c638..ed16a41 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ Integrating Pipelines with any OpenAI API-compatible UI client is simple. Launch ## ⚡ Quick Start with Docker +> [!WARNING] +> Pipelines are a plugin system with arbitrary code execution — **don't fetch random pipelines from sources you don't trust**. + For a streamlined setup using Docker: 1. **Run the Pipelines container:** @@ -33,12 +36,12 @@ For a streamlined setup using Docker: 2. **Connect to Open WebUI:** - Navigate to the **Settings > Connections > OpenAI API** section in Open WebUI. - - Set the API URL to `http://localhost:9099` and the API key to `0p3n-w3bu!`. Your filter should now be active. + - Set the API URL to `http://localhost:9099` and the API key to `0p3n-w3bu!`. Your pipelines should now be active. 3. **Manage Configurations:** - In the admin panel, go to **Admin Settings > Pipelines tab**. - - Select your desired filter and modify the valve values directly from the WebUI. + - Select your desired pipeline and modify the valve values directly from the WebUI. If you need to install a custom pipeline with additional dependencies: @@ -48,6 +51,8 @@ If you need to install a custom pipeline with additional dependencies: docker run -d -p 9099:9099 -e PIPELINES_PATH="https://github.com/open-webui/pipelines/blob/main/examples/filters/detoxify_filter_pipeline.py" -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main ``` +Alternatively, you can directly install pipelines from the admin settings by copying and pasting the pipeline URL, provided it doesn't have additional dependencies. + That's it! You're now ready to build customizable AI integrations effortlessly with Pipelines. Enjoy! ## 📦 Installation and Setup From fd4eef4bb216908c0f484f3686777c67870d0eb5 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 13:53:41 -0700 Subject: [PATCH 09/51] doc: readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed16a41..69a68af 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ## 🚀 Why Choose Pipelines? -- **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. - **Limitless Possibilities:** Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs. +- **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. - **Custom Hooks:** Build and integrate custom pipelines. ## 🔧 How It Works @@ -20,6 +20,14 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat Integrating Pipelines with any OpenAI API-compatible UI client is simple. Launch your Pipelines instance and set the OpenAI URL on your client to the Pipelines URL. That's it! You're ready to leverage any Python library for your needs. +### Examples of What You Can Achieve: + +- **Rate Limit Filter**: Control the flow of requests to prevent exceeding rate limits. +- **Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions. +- **Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs. +- **Function Calling Filter**: Easily handle function calls and enhance your applications with custom logic. +- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. + ## ⚡ Quick Start with Docker > [!WARNING] From 3205de817b163538a05944cd87b3ad39918fe7cf Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 13:58:24 -0700 Subject: [PATCH 10/51] doc: readme --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 69a68af..dbc2044 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat - **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. - **Custom Hooks:** Build and integrate custom pipelines. +### Examples of What You Can Achieve: + +- [**Rate Limit Filter**: Control the flow of requests to prevent exceeding rate limits.](/examples/filters/rate_limit_filter_pipeline.py) +- [**Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions.](/examples/filters/libretranslate_filter_pipeline.py) +- [**Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs.](/examples/rag/llamaindex_pipeline.py) +- [**Function Calling Filter**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) +- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. + ## 🔧 How It Works

@@ -20,14 +28,6 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat Integrating Pipelines with any OpenAI API-compatible UI client is simple. Launch your Pipelines instance and set the OpenAI URL on your client to the Pipelines URL. That's it! You're ready to leverage any Python library for your needs. -### Examples of What You Can Achieve: - -- **Rate Limit Filter**: Control the flow of requests to prevent exceeding rate limits. -- **Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions. -- **Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs. -- **Function Calling Filter**: Easily handle function calls and enhance your applications with custom logic. -- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. - ## ⚡ Quick Start with Docker > [!WARNING] From 288282082b27e5f0635578a13222f59f3e9d3f5f Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 14:09:06 -0700 Subject: [PATCH 11/51] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dbc2044..e3086b3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat - [**Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions.](/examples/filters/libretranslate_filter_pipeline.py) - [**Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs.](/examples/rag/llamaindex_pipeline.py) - [**Function Calling Filter**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) -- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. +- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. [Check out our scaffolds to get a head start on your projects and see how you can streamline your development process!](/examples/scaffolds) ## 🔧 How It Works From 3719644c6351492592001c96bb363afeaa27be1d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 14:26:43 -0700 Subject: [PATCH 12/51] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3086b3..1bee6b9 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ If you need to install a custom pipeline with additional dependencies: - **Run the following command:** ```sh - docker run -d -p 9099:9099 -e PIPELINES_PATH="https://github.com/open-webui/pipelines/blob/main/examples/filters/detoxify_filter_pipeline.py" -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main + docker run -d -p 9099:9099 -e PIPELINES_URLS="https://github.com/open-webui/pipelines/blob/main/examples/filters/detoxify_filter_pipeline.py" -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main ``` Alternatively, you can directly install pipelines from the admin settings by copying and pasting the pipeline URL, provided it doesn't have additional dependencies. From a248b96dba26e5e078b6535933e3527e949b4374 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 14:34:48 -0700 Subject: [PATCH 13/51] refac --- main.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index dd3381f..6cec49c 100644 --- a/main.py +++ b/main.py @@ -144,16 +144,17 @@ async def load_modules_from_directory(directory): if os.path.exists(valves_json_path): with open(valves_json_path, "r") as f: valves_json = json.load(f) - ValvesModel = pipeline.valves.__class__ - # Create a ValvesModel instance using default values and overwrite with valves_json - combined_valves = { - **pipeline.valves.model_dump(), - **valves_json, - } - valves = ValvesModel(**combined_valves) - pipeline.valves = valves + if hasattr(pipeline, "valves"): + ValvesModel = pipeline.valves.__class__ + # Create a ValvesModel instance using default values and overwrite with valves_json + combined_valves = { + **pipeline.valves.model_dump(), + **valves_json, + } + valves = ValvesModel(**combined_valves) + pipeline.valves = valves - logging.info(f"Updated valves for module: {module_name}") + logging.info(f"Updated valves for module: {module_name}") pipeline_id = pipeline.id if hasattr(pipeline, "id") else module_name PIPELINE_MODULES[pipeline_id] = pipeline From 871348049f3071f260496d04b89b539642493079 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 14:38:01 -0700 Subject: [PATCH 14/51] enh: current time fc --- .../function_calling_filter_pipeline.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/function_calling/function_calling_filter_pipeline.py b/examples/function_calling/function_calling_filter_pipeline.py index 8db3da5..a0f5298 100644 --- a/examples/function_calling/function_calling_filter_pipeline.py +++ b/examples/function_calling/function_calling_filter_pipeline.py @@ -1,6 +1,9 @@ import os import requests from typing import Literal, List, Optional +from datetime import datetime + + from blueprints.function_calling_blueprint import Pipeline as FunctionCallingBlueprint @@ -14,6 +17,19 @@ class Pipeline(FunctionCallingBlueprint): def __init__(self, pipeline) -> None: self.pipeline = pipeline + def get_current_time( + self, + ) -> str: + """ + Get the current time. + + :return: The current time. + """ + + now = datetime.now() + current_time = now.strftime("%H:%M:%S") + return f"Current Time = {current_time}" + def get_current_weather( self, location: str, From 8d830941ab8c02c850e66ed3698cdfba5d681f50 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 15:06:57 -0700 Subject: [PATCH 15/51] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bee6b9..86244c5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat - [**Rate Limit Filter**: Control the flow of requests to prevent exceeding rate limits.](/examples/filters/rate_limit_filter_pipeline.py) - [**Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions.](/examples/filters/libretranslate_filter_pipeline.py) - [**Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs.](/examples/rag/llamaindex_pipeline.py) -- [**Function Calling Filter**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) +- [**Function Calling Pipeline**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) - **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. [Check out our scaffolds to get a head start on your projects and see how you can streamline your development process!](/examples/scaffolds) ## 🔧 How It Works From aa74b9635a9567ca7d16067ae4c7da90bb82ccd4 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 16:40:34 -0700 Subject: [PATCH 16/51] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86244c5..4ed23da 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ## 🔧 How It Works

Integrating Pipelines with any OpenAI API-compatible UI client is simple. Launch your Pipelines instance and set the OpenAI URL on your client to the Pipelines URL. That's it! You're ready to leverage any Python library for your needs. From 0cfb82feb3180fd14e2c29d417e9193875abe596 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 16:52:44 -0700 Subject: [PATCH 17/51] doc: readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ed23da..a40fda0 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,12 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ### Examples of What You Can Achieve: +- [**Function Calling Pipeline**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) +- [**Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs.](/examples/rag/llamaindex_pipeline.py) +- [**Message Monitoring Using Langfuse**: Monitor and analyze message interactions in real-time using Langfuse.](/examples/filters/langfuse_filter_pipeline.py) - [**Rate Limit Filter**: Control the flow of requests to prevent exceeding rate limits.](/examples/filters/rate_limit_filter_pipeline.py) - [**Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions.](/examples/filters/libretranslate_filter_pipeline.py) -- [**Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs.](/examples/rag/llamaindex_pipeline.py) -- [**Function Calling Pipeline**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) +- [**Toxic Message Filter**: Implement filters to detect and handle toxic messages effectively.](/examples/filters/detoxify_filter_pipeline.py) - **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. [Check out our scaffolds to get a head start on your projects and see how you can streamline your development process!](/examples/scaffolds) ## 🔧 How It Works From 68f15be92babd5a677f97e64e424fc694486a011 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 16:58:19 -0700 Subject: [PATCH 18/51] doc: readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a40fda0..ee79740 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ### Examples of What You Can Achieve: -- [**Function Calling Pipeline**: Easily handle function calls and enhance your applications with custom logic.](/examples/function_calling/function_calling_filter_pipeline.py) -- [**Custom RAG Pipeline**: Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs.](/examples/rag/llamaindex_pipeline.py) -- [**Message Monitoring Using Langfuse**: Monitor and analyze message interactions in real-time using Langfuse.](/examples/filters/langfuse_filter_pipeline.py) -- [**Rate Limit Filter**: Control the flow of requests to prevent exceeding rate limits.](/examples/filters/rate_limit_filter_pipeline.py) -- [**Real-Time Translation Filter with LibreTranslate**: Seamlessly integrate real-time translations into your LLM interactions.](/examples/filters/libretranslate_filter_pipeline.py) -- [**Toxic Message Filter**: Implement filters to detect and handle toxic messages effectively.](/examples/filters/detoxify_filter_pipeline.py) -- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. [Check out our scaffolds to get a head start on your projects and see how you can streamline your development process!](/examples/scaffolds) +- [**Function Calling Pipeline**](/examples/function_calling/function_calling_filter_pipeline.py): Easily handle function calls and enhance your applications with custom logic. +- [**Custom RAG Pipeline**](/examples/rag/llamaindex_pipeline.py): Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs. +- [**Message Monitoring Using Langfuse**](/examples/filters/langfuse_filter_pipeline.py): Monitor and analyze message interactions in real-time using Langfuse. +- [**Rate Limit Filter**](/examples/filters/rate_limit_filter_pipeline.py): Control the flow of requests to prevent exceeding rate limits. +- [**Real-Time Translation Filter with LibreTranslate**](/examples/filters/libretranslate_filter_pipeline.py): Seamlessly integrate real-time translations into your LLM interactions. +- [**Toxic Message Filter**](/examples/filters/detoxify_filter_pipeline.py): Implement filters to detect and handle toxic messages effectively. +- **And Much More!**: The sky is the limit for what you can accomplish with Pipelines and Python. [Check out our scaffolds](/examples/scaffolds) to get a head start on your projects and see how you can streamline your development process! ## 🔧 How It Works From fb7ffc9dc3dbbfd54921710f928e5c2c9fd200cd Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 17:03:03 -0700 Subject: [PATCH 19/51] refac --- examples/scaffolds/function_calling_scaffold.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/scaffolds/function_calling_scaffold.py b/examples/scaffolds/function_calling_scaffold.py index 387d49f..2c9f726 100644 --- a/examples/scaffolds/function_calling_scaffold.py +++ b/examples/scaffolds/function_calling_scaffold.py @@ -3,7 +3,7 @@ from blueprints.function_calling_blueprint import Pipeline as FunctionCallingBlu class Pipeline(FunctionCallingBlueprint): class Valves(FunctionCallingBlueprint.Valves): - # Add your custom parameters here + # Add your custom valves here pass class Tools: @@ -11,6 +11,7 @@ class Pipeline(FunctionCallingBlueprint): self.pipeline = pipeline # Add your custom tools using pure Python code here, make sure to add type hints + # Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications # Please refer to function_calling_filter_pipeline.py for an example pass From 76ba2b407a7ab13fd476bcbc059b61a0a7c5a311 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 21:47:08 -0700 Subject: [PATCH 20/51] feat: RESET_PIPELINES_DIR env var --- start.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/start.sh b/start.sh index bc21188..9b71a22 100755 --- a/start.sh +++ b/start.sh @@ -1,6 +1,33 @@ #!/usr/bin/env bash PORT="${PORT:-9099}" HOST="${HOST:-0.0.0.0}" +# Default value for PIPELINES_DIR +PIPELINES_DIR=${PIPELINES_DIR:-./pipelines} + +# Function to reset pipelines +reset_pipelines_dir() { + if [ "$RESET_PIPELINES_DIR" = true ]; then + echo "Resetting pipelines directory: $PIPELINES_DIR" + + # Check if the directory exists + if [ -d "$PIPELINES_DIR" ]; then + # Remove all contents of the directory + rm -rf "${PIPELINES_DIR:?}"/* + echo "All contents in $PIPELINES_DIR have been removed." + + # Optionally recreate the directory if needed + mkdir -p "$PIPELINES_DIR" + echo "$PIPELINES_DIR has been recreated." + else + echo "Directory $PIPELINES_DIR does not exist. No action taken." + fi + else + echo "RESET_PIPELINES_DIR is not set to true. No action taken." + fi +} + +# Example usage of the function +reset_pipelines_dir # Function to install requirements if requirements.txt is provided install_requirements() { From f627142ccb3e186615dbd5899a34487e65540443 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 22:41:02 -0700 Subject: [PATCH 21/51] doc: readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee79740..ecba440 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ For a streamlined setup using Docker: 1. **Run the Pipelines container:** ```sh - docker run -d -p 9099:9099 -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main + docker run -d -p 9099:9099 --add-host=host.docker.internal:host-gateway -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main ``` 2. **Connect to Open WebUI:** @@ -58,7 +58,7 @@ If you need to install a custom pipeline with additional dependencies: - **Run the following command:** ```sh - docker run -d -p 9099:9099 -e PIPELINES_URLS="https://github.com/open-webui/pipelines/blob/main/examples/filters/detoxify_filter_pipeline.py" -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main + docker run -d -p 9099:9099 --add-host=host.docker.internal:host-gateway -e PIPELINES_URLS="https://github.com/open-webui/pipelines/blob/main/examples/filters/detoxify_filter_pipeline.py" -v pipelines:/app/pipelines --name pipelines --restart always ghcr.io/open-webui/pipelines:main ``` Alternatively, you can directly install pipelines from the admin settings by copying and pasting the pipeline URL, provided it doesn't have additional dependencies. From c4b5f2be476c9f4494d9c105e6b78c6e01849f60 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sat, 1 Jun 2024 22:59:27 -0700 Subject: [PATCH 22/51] enh: azure openai pipeline --- examples/providers/azure_openai_pipeline.py | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/providers/azure_openai_pipeline.py b/examples/providers/azure_openai_pipeline.py index 994c378..088d8a8 100644 --- a/examples/providers/azure_openai_pipeline.py +++ b/examples/providers/azure_openai_pipeline.py @@ -1,9 +1,19 @@ from typing import List, Union, Generator, Iterator from schemas import OpenAIChatMessage +from pydantic import BaseModel import requests class Pipeline: + class Valves(BaseModel): + # You can add your custom valves here. + AZURE_OPENAI_API_KEY: str = "your-azure-openai-api-key-here" + AZURE_OPENAI_ENDPOINT: str = "your-azure-openai-endpoint-here" + DEPLOYMENT_NAME: str = "your-deployment-name-here" + API_VERSION: str = "2023-10-01-preview" + MODEL: str = "gpt-3.5-turbo" + pass + def __init__(self): # Optionally, you can set the id and name of the pipeline. # Assign a unique identifier to the pipeline. @@ -11,6 +21,7 @@ class Pipeline: # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. self.id = "azure_openai_pipeline" self.name = "Azure OpenAI Pipeline" + self.valves = self.Valves() pass async def on_startup(self): @@ -32,25 +43,22 @@ class Pipeline: print(messages) print(user_message) - AZURE_OPENAI_API_KEY = "your-azure-openai-api-key-here" - AZURE_OPENAI_ENDPOINT = "your-azure-openai-endpoint-here" - DEPLOYMENT_NAME = "your-deployment-name-here" - MODEL = "gpt-3.5-turbo" + headers = { + "api-key": self.valves.AZURE_OPENAI_API_KEY, + "Content-Type": "application/json", + } - headers = {"api-key": AZURE_OPENAI_API_KEY, "Content-Type": "application/json"} - - url = f"{AZURE_OPENAI_ENDPOINT}/openai/deployments/{DEPLOYMENT_NAME}/chat/completions?api-version=2023-10-01-preview" + url = f"{self.valves.AZURE_OPENAI_ENDPOINT}/openai/deployments/{self.valves.DEPLOYMENT_NAME}/chat/completions?api-version={self.valves.API_VERSION}" try: r = requests.post( url=url, - json={**body, "model": MODEL}, + json={**body, "model": self.valves.MODEL}, headers=headers, stream=True, ) r.raise_for_status() - if body["stream"]: return r.iter_lines() else: From db40108c596329990f356fbab1edff5edd1efadb Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 00:05:36 -0700 Subject: [PATCH 23/51] Update langfuse_filter_pipeline.py --- examples/filters/langfuse_filter_pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/filters/langfuse_filter_pipeline.py b/examples/filters/langfuse_filter_pipeline.py index 6dd4894..706f591 100644 --- a/examples/filters/langfuse_filter_pipeline.py +++ b/examples/filters/langfuse_filter_pipeline.py @@ -51,8 +51,8 @@ class Pipeline: self.valves = self.Valves( **{ "pipelines": ["*"], # Connect to all pipelines - "secret_key": os.getenv("LANGFUSE_SECRET_KEY"), - "public_key": os.getenv("LANGFUSE_PUBLIC_KEY"), + "secret_key": os.getenv("LANGFUSE_SECRET_KEY", "your-secret-key-here"), + "public_key": os.getenv("LANGFUSE_PUBLIC_KEY", "your-public-key-here"), "host": os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com"), } ) From b1a56a53763308c9eb89b0e745e7882ac2d88b1c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 10:49:40 -0700 Subject: [PATCH 24/51] doc: readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ecba440..5da55bf 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,17 @@ For a streamlined setup using Docker: - Navigate to the **Settings > Connections > OpenAI API** section in Open WebUI. - Set the API URL to `http://localhost:9099` and the API key to `0p3n-w3bu!`. Your pipelines should now be active. + > [!NOTE] + > If your Open WebUI is running in a Docker container, replace `localhost` with `host.docker.internal` in the API URL. + 3. **Manage Configurations:** - In the admin panel, go to **Admin Settings > Pipelines tab**. - Select your desired pipeline and modify the valve values directly from the WebUI. +> [!TIP] +> If you are unable to connect, it is most likely a Docker networking issue. We encourage you to troubleshoot on your own and share your methods and solutions in the discussions forum. + If you need to install a custom pipeline with additional dependencies: - **Run the following command:** From 179c66db17f4a2de915e974db06b623db5718b7a Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 10:50:23 -0700 Subject: [PATCH 25/51] doc: fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5da55bf..17c1290 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ For a streamlined setup using Docker: - Navigate to the **Settings > Connections > OpenAI API** section in Open WebUI. - Set the API URL to `http://localhost:9099` and the API key to `0p3n-w3bu!`. Your pipelines should now be active. - > [!NOTE] - > If your Open WebUI is running in a Docker container, replace `localhost` with `host.docker.internal` in the API URL. +> [!NOTE] +> If your Open WebUI is running in a Docker container, replace `localhost` with `host.docker.internal` in the API URL. 3. **Manage Configurations:** From 5478b920df5fdc26d69a185a440ebde05d013a9d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 11:40:24 -0700 Subject: [PATCH 26/51] enh: langfuse filter --- examples/filters/langfuse_filter_pipeline.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/filters/langfuse_filter_pipeline.py b/examples/filters/langfuse_filter_pipeline.py index 706f591..eb8e751 100644 --- a/examples/filters/langfuse_filter_pipeline.py +++ b/examples/filters/langfuse_filter_pipeline.py @@ -101,3 +101,18 @@ class Pipeline: print(trace.get_trace_url()) return body + + async def outlet(self, body: dict, user: Optional[dict] = None) -> dict: + print(f"outlet:{__name__}") + + trace = self.langfuse.trace( + name=f"filter:{__name__}", + input=body, + user_id=user["id"], + metadata={"name": user["name"]}, + session_id=body["chat_id"], + ) + + print(trace.get_trace_url()) + + return body From dd98a71d0b731c22bf0925ccc6fd2f76172c1fc7 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 11:43:41 -0700 Subject: [PATCH 27/51] refac --- blueprints/function_calling_blueprint.py | 2 +- .../filters/libretranslate_filter_pipeline.py | 2 +- main.py | 22 +++++++++++++------ utils/{ => pipelines}/auth.py | 0 utils/{ => pipelines}/main.py | 0 utils/{ => pipelines}/misc.py | 0 6 files changed, 17 insertions(+), 9 deletions(-) rename utils/{ => pipelines}/auth.py (100%) rename utils/{ => pipelines}/main.py (100%) rename utils/{ => pipelines}/misc.py (100%) diff --git a/blueprints/function_calling_blueprint.py b/blueprints/function_calling_blueprint.py index aeae241..f9ad77d 100644 --- a/blueprints/function_calling_blueprint.py +++ b/blueprints/function_calling_blueprint.py @@ -5,7 +5,7 @@ import os import requests import json -from utils.main import ( +from utils.pipelines.main import ( get_last_user_message, add_or_update_system_message, get_tools_specs, diff --git a/examples/filters/libretranslate_filter_pipeline.py b/examples/filters/libretranslate_filter_pipeline.py index 0c14ac8..37a3547 100644 --- a/examples/filters/libretranslate_filter_pipeline.py +++ b/examples/filters/libretranslate_filter_pipeline.py @@ -4,7 +4,7 @@ from pydantic import BaseModel import requests import os -from utils.main import get_last_user_message, get_last_assistant_message +from utils.pipelines.main import get_last_user_message, get_last_assistant_message class Pipeline: diff --git a/main.py b/main.py index 6cec49c..acccffe 100644 --- a/main.py +++ b/main.py @@ -8,9 +8,9 @@ from pydantic import BaseModel, ConfigDict from typing import List, Union, Generator, Iterator -from utils.auth import bearer_security, get_current_user -from utils.main import get_last_user_message, stream_message_template -from utils.misc import convert_to_raw_url +from utils.pipelines.auth import bearer_security, get_current_user +from utils.pipelines.main import get_last_user_message, stream_message_template +from utils.pipelines.misc import convert_to_raw_url from contextlib import asynccontextmanager from concurrent.futures import ThreadPoolExecutor @@ -108,11 +108,19 @@ def get_all_pipelines(): async def load_module_from_path(module_name, module_path): spec = importlib.util.spec_from_file_location(module_name, module_path) module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - print(f"Loaded module: {module.__name__}") - if hasattr(module, "Pipeline"): - return module.Pipeline() + try: + spec.loader.exec_module(module) + print(f"Loaded module: {module.__name__}") + if hasattr(module, "Pipeline"): + return module.Pipeline() + else: + raise Exception("No Pipeline class found") + + except Exception as e: + print(f"Error loading module: {module_name}") + print(e) + os.remove(module_path) return None diff --git a/utils/auth.py b/utils/pipelines/auth.py similarity index 100% rename from utils/auth.py rename to utils/pipelines/auth.py diff --git a/utils/main.py b/utils/pipelines/main.py similarity index 100% rename from utils/main.py rename to utils/pipelines/main.py diff --git a/utils/misc.py b/utils/pipelines/misc.py similarity index 100% rename from utils/misc.py rename to utils/pipelines/misc.py From 62d0d138d28b0c31a2db806b680e85a2489c9c1d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 11:47:32 -0700 Subject: [PATCH 28/51] refac: shouldn't remove files --- main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/main.py b/main.py index acccffe..6e5c23b 100644 --- a/main.py +++ b/main.py @@ -116,11 +116,9 @@ async def load_module_from_path(module_name, module_path): return module.Pipeline() else: raise Exception("No Pipeline class found") - except Exception as e: print(f"Error loading module: {module_name}") print(e) - os.remove(module_path) return None From 418572bfaf8f4220afb3528b103100d2d114629b Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 12:02:59 -0700 Subject: [PATCH 29/51] enh: langfuse --- examples/filters/langfuse_filter_pipeline.py | 39 ++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/examples/filters/langfuse_filter_pipeline.py b/examples/filters/langfuse_filter_pipeline.py index eb8e751..f88cad3 100644 --- a/examples/filters/langfuse_filter_pipeline.py +++ b/examples/filters/langfuse_filter_pipeline.py @@ -2,7 +2,7 @@ title: Langfuse Filter Pipeline author: open-webui date: 2024-05-30 -version: 1.0 +version: 1.1 license: MIT description: A filter pipeline that uses Langfuse. requirements: langfuse @@ -12,13 +12,12 @@ from typing import List, Optional from schemas import OpenAIChatMessage import os - +from utils.pipelines.main import get_last_user_message, get_last_assistant_message from pydantic import BaseModel from langfuse import Langfuse class Pipeline: - class Valves(BaseModel): # List target pipeline ids (models) that this filter will be connected to. # If you want to connect this filter to all pipelines, you can set pipelines to ["*"] @@ -58,6 +57,7 @@ class Pipeline: ) self.langfuse = None + self.chat_generations = {} pass async def on_startup(self): @@ -98,21 +98,38 @@ class Pipeline: session_id=body["chat_id"], ) + generation = trace.generation( + name=body["chat_id"], + model=body["model"], + input=body["messages"], + metadata={"interface": "open-webui"}, + ) + + self.chat_generations[body["chat_id"]] = generation print(trace.get_trace_url()) return body async def outlet(self, body: dict, user: Optional[dict] = None) -> dict: print(f"outlet:{__name__}") + if body["chat_id"] not in self.chat_generations: + return body - trace = self.langfuse.trace( - name=f"filter:{__name__}", - input=body, - user_id=user["id"], - metadata={"name": user["name"]}, - session_id=body["chat_id"], + generation = self.chat_generations[body["chat_id"]] + + user_message = get_last_user_message(body["messages"]) + generated_message = get_last_assistant_message(body["messages"]) + + # Update usage cost based on the length of the input and output messages + # Below does not reflect the actual cost of the API + # You can adjust the cost based on your requirements + generation.end( + output=generated_message, + usage={ + "totalCost": (len(user_message) + len(generated_message)) / 1000, + "unit": "CHARACTERS", + }, + metadata={"interface": "open-webui"}, ) - print(trace.get_trace_url()) - return body From d0c2e3dc319466e049539975e7893800039c7104 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 13:32:58 -0700 Subject: [PATCH 30/51] feat: tiktoken --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 06cc026..5f27030 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,6 +43,7 @@ sentence-transformers transformers tokenizers nltk +tiktoken # Image processing Pillow From 7dddb949cd55b1720bb6bbdb3812249232fe1a88 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 14:22:48 -0700 Subject: [PATCH 31/51] refac: examples dir --- .../function_calling_filter_pipeline.py | 0 .../integrations}/applescript_pipeline.py | 0 .../integrations}/python_code_pipeline.py | 0 .../{automation => pipelines/integrations}/wikipedia_pipeline.py | 0 examples/{ => pipelines}/providers/anthropic_manifold_pipeline.py | 0 examples/{ => pipelines}/providers/azure_openai_pipeline.py | 0 examples/{ => pipelines}/providers/cohere_manifold_pipeline.py | 0 examples/{ => pipelines}/providers/litellm_manifold_pipeline.py | 0 .../providers/litellm_subprocess_manifold_pipeline.py | 0 examples/{ => pipelines}/providers/llama_cpp_pipeline.py | 0 examples/{ => pipelines}/providers/mlx_pipeline.py | 0 examples/{ => pipelines}/providers/ollama_manifold_pipeline.py | 0 examples/{ => pipelines}/providers/ollama_pipeline.py | 0 examples/{ => pipelines}/providers/openai_pipeline.py | 0 examples/{ => pipelines}/rag/haystack_pipeline.py | 0 examples/{ => pipelines}/rag/llamaindex_ollama_github_pipeline.py | 0 examples/{ => pipelines}/rag/llamaindex_ollama_pipeline.py | 0 examples/{ => pipelines}/rag/llamaindex_pipeline.py | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename examples/{function_calling => filters}/function_calling_filter_pipeline.py (100%) rename examples/{automation => pipelines/integrations}/applescript_pipeline.py (100%) rename examples/{automation => pipelines/integrations}/python_code_pipeline.py (100%) rename examples/{automation => pipelines/integrations}/wikipedia_pipeline.py (100%) rename examples/{ => pipelines}/providers/anthropic_manifold_pipeline.py (100%) rename examples/{ => pipelines}/providers/azure_openai_pipeline.py (100%) rename examples/{ => pipelines}/providers/cohere_manifold_pipeline.py (100%) rename examples/{ => pipelines}/providers/litellm_manifold_pipeline.py (100%) rename examples/{ => pipelines}/providers/litellm_subprocess_manifold_pipeline.py (100%) rename examples/{ => pipelines}/providers/llama_cpp_pipeline.py (100%) rename examples/{ => pipelines}/providers/mlx_pipeline.py (100%) rename examples/{ => pipelines}/providers/ollama_manifold_pipeline.py (100%) rename examples/{ => pipelines}/providers/ollama_pipeline.py (100%) rename examples/{ => pipelines}/providers/openai_pipeline.py (100%) rename examples/{ => pipelines}/rag/haystack_pipeline.py (100%) rename examples/{ => pipelines}/rag/llamaindex_ollama_github_pipeline.py (100%) rename examples/{ => pipelines}/rag/llamaindex_ollama_pipeline.py (100%) rename examples/{ => pipelines}/rag/llamaindex_pipeline.py (100%) diff --git a/examples/function_calling/function_calling_filter_pipeline.py b/examples/filters/function_calling_filter_pipeline.py similarity index 100% rename from examples/function_calling/function_calling_filter_pipeline.py rename to examples/filters/function_calling_filter_pipeline.py diff --git a/examples/automation/applescript_pipeline.py b/examples/pipelines/integrations/applescript_pipeline.py similarity index 100% rename from examples/automation/applescript_pipeline.py rename to examples/pipelines/integrations/applescript_pipeline.py diff --git a/examples/automation/python_code_pipeline.py b/examples/pipelines/integrations/python_code_pipeline.py similarity index 100% rename from examples/automation/python_code_pipeline.py rename to examples/pipelines/integrations/python_code_pipeline.py diff --git a/examples/automation/wikipedia_pipeline.py b/examples/pipelines/integrations/wikipedia_pipeline.py similarity index 100% rename from examples/automation/wikipedia_pipeline.py rename to examples/pipelines/integrations/wikipedia_pipeline.py diff --git a/examples/providers/anthropic_manifold_pipeline.py b/examples/pipelines/providers/anthropic_manifold_pipeline.py similarity index 100% rename from examples/providers/anthropic_manifold_pipeline.py rename to examples/pipelines/providers/anthropic_manifold_pipeline.py diff --git a/examples/providers/azure_openai_pipeline.py b/examples/pipelines/providers/azure_openai_pipeline.py similarity index 100% rename from examples/providers/azure_openai_pipeline.py rename to examples/pipelines/providers/azure_openai_pipeline.py diff --git a/examples/providers/cohere_manifold_pipeline.py b/examples/pipelines/providers/cohere_manifold_pipeline.py similarity index 100% rename from examples/providers/cohere_manifold_pipeline.py rename to examples/pipelines/providers/cohere_manifold_pipeline.py diff --git a/examples/providers/litellm_manifold_pipeline.py b/examples/pipelines/providers/litellm_manifold_pipeline.py similarity index 100% rename from examples/providers/litellm_manifold_pipeline.py rename to examples/pipelines/providers/litellm_manifold_pipeline.py diff --git a/examples/providers/litellm_subprocess_manifold_pipeline.py b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py similarity index 100% rename from examples/providers/litellm_subprocess_manifold_pipeline.py rename to examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py diff --git a/examples/providers/llama_cpp_pipeline.py b/examples/pipelines/providers/llama_cpp_pipeline.py similarity index 100% rename from examples/providers/llama_cpp_pipeline.py rename to examples/pipelines/providers/llama_cpp_pipeline.py diff --git a/examples/providers/mlx_pipeline.py b/examples/pipelines/providers/mlx_pipeline.py similarity index 100% rename from examples/providers/mlx_pipeline.py rename to examples/pipelines/providers/mlx_pipeline.py diff --git a/examples/providers/ollama_manifold_pipeline.py b/examples/pipelines/providers/ollama_manifold_pipeline.py similarity index 100% rename from examples/providers/ollama_manifold_pipeline.py rename to examples/pipelines/providers/ollama_manifold_pipeline.py diff --git a/examples/providers/ollama_pipeline.py b/examples/pipelines/providers/ollama_pipeline.py similarity index 100% rename from examples/providers/ollama_pipeline.py rename to examples/pipelines/providers/ollama_pipeline.py diff --git a/examples/providers/openai_pipeline.py b/examples/pipelines/providers/openai_pipeline.py similarity index 100% rename from examples/providers/openai_pipeline.py rename to examples/pipelines/providers/openai_pipeline.py diff --git a/examples/rag/haystack_pipeline.py b/examples/pipelines/rag/haystack_pipeline.py similarity index 100% rename from examples/rag/haystack_pipeline.py rename to examples/pipelines/rag/haystack_pipeline.py diff --git a/examples/rag/llamaindex_ollama_github_pipeline.py b/examples/pipelines/rag/llamaindex_ollama_github_pipeline.py similarity index 100% rename from examples/rag/llamaindex_ollama_github_pipeline.py rename to examples/pipelines/rag/llamaindex_ollama_github_pipeline.py diff --git a/examples/rag/llamaindex_ollama_pipeline.py b/examples/pipelines/rag/llamaindex_ollama_pipeline.py similarity index 100% rename from examples/rag/llamaindex_ollama_pipeline.py rename to examples/pipelines/rag/llamaindex_ollama_pipeline.py diff --git a/examples/rag/llamaindex_pipeline.py b/examples/pipelines/rag/llamaindex_pipeline.py similarity index 100% rename from examples/rag/llamaindex_pipeline.py rename to examples/pipelines/rag/llamaindex_pipeline.py From 4c219762536ed5805f181aeec0272c7314dc2078 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 14:23:49 -0700 Subject: [PATCH 32/51] doc: readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 17c1290..85e3533 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ### Examples of What You Can Achieve: -- [**Function Calling Pipeline**](/examples/function_calling/function_calling_filter_pipeline.py): Easily handle function calls and enhance your applications with custom logic. -- [**Custom RAG Pipeline**](/examples/rag/llamaindex_pipeline.py): Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs. +- [**Function Calling Pipeline**](/examples/filters/function_calling_filter_pipeline.py): Easily handle function calls and enhance your applications with custom logic. +- [**Custom RAG Pipeline**](/examples/pipelines/rag/llamaindex_pipeline.py): Implement sophisticated Retrieval-Augmented Generation pipelines tailored to your needs. - [**Message Monitoring Using Langfuse**](/examples/filters/langfuse_filter_pipeline.py): Monitor and analyze message interactions in real-time using Langfuse. - [**Rate Limit Filter**](/examples/filters/rate_limit_filter_pipeline.py): Control the flow of requests to prevent exceeding rate limits. - [**Real-Time Translation Filter with LibreTranslate**](/examples/filters/libretranslate_filter_pipeline.py): Seamlessly integrate real-time translations into your LLM interactions. From 79fa14acbe71789e56c10544539407b104331c65 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 15:03:08 -0700 Subject: [PATCH 33/51] chore: comments --- blueprints/function_calling_blueprint.py | 5 +++-- examples/filters/conversation_turn_limit_filter.py | 5 +++-- examples/filters/detoxify_filter_pipeline.py | 4 ++-- examples/filters/function_calling_filter_pipeline.py | 6 +++++- examples/filters/langfuse_filter_pipeline.py | 4 ++-- examples/filters/libretranslate_filter_pipeline.py | 4 ++-- examples/filters/rate_limit_filter_pipeline.py | 5 +++-- examples/pipelines/integrations/applescript_pipeline.py | 5 ++--- examples/pipelines/integrations/python_code_pipeline.py | 5 ++++- examples/pipelines/integrations/wikipedia_pipeline.py | 5 +++-- examples/pipelines/providers/azure_openai_pipeline.py | 4 ++-- examples/pipelines/providers/cohere_manifold_pipeline.py | 8 +++++++- examples/pipelines/providers/litellm_manifold_pipeline.py | 4 ++-- .../providers/litellm_subprocess_manifold_pipeline.py | 4 ++-- examples/pipelines/providers/llama_cpp_pipeline.py | 4 ++-- examples/pipelines/providers/mlx_pipeline.py | 4 ++-- examples/pipelines/providers/ollama_manifold_pipeline.py | 4 ++-- examples/pipelines/providers/ollama_pipeline.py | 4 ++-- examples/pipelines/providers/openai_pipeline.py | 4 ++-- examples/scaffolds/example_pipeline_scaffold.py | 7 ++++--- examples/scaffolds/filter_pipeline_scaffold.py | 5 +++-- examples/scaffolds/function_calling_scaffold.py | 7 ++++++- examples/scaffolds/manifold_pipeline_scaffold.py | 4 ++-- 23 files changed, 67 insertions(+), 44 deletions(-) diff --git a/blueprints/function_calling_blueprint.py b/blueprints/function_calling_blueprint.py index f9ad77d..4e0f496 100644 --- a/blueprints/function_calling_blueprint.py +++ b/blueprints/function_calling_blueprint.py @@ -34,10 +34,11 @@ class Pipeline: # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. self.type = "filter" - # Assign a unique identifier to the pipeline. + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "function_calling_blueprint" + # self.id = "function_calling_blueprint" self.name = "Function Calling Blueprint" # Initialize valves diff --git a/examples/filters/conversation_turn_limit_filter.py b/examples/filters/conversation_turn_limit_filter.py index 54e9483..bb31939 100644 --- a/examples/filters/conversation_turn_limit_filter.py +++ b/examples/filters/conversation_turn_limit_filter.py @@ -25,10 +25,11 @@ class Pipeline: # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. self.type = "filter" - # Assign a unique identifier to the pipeline. + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "conversation_turn_limit_filter_pipeline" + # self.id = "conversation_turn_limit_filter_pipeline" self.name = "Conversation Turn Limit Filter" self.valves = self.Valves( diff --git a/examples/filters/detoxify_filter_pipeline.py b/examples/filters/detoxify_filter_pipeline.py index 7411b39..73fc3a9 100644 --- a/examples/filters/detoxify_filter_pipeline.py +++ b/examples/filters/detoxify_filter_pipeline.py @@ -33,10 +33,10 @@ class Pipeline: self.type = "filter" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "detoxify_filter_pipeline" + # self.id = "detoxify_filter_pipeline" self.name = "Detoxify Filter" # Initialize diff --git a/examples/filters/function_calling_filter_pipeline.py b/examples/filters/function_calling_filter_pipeline.py index a0f5298..5ea9957 100644 --- a/examples/filters/function_calling_filter_pipeline.py +++ b/examples/filters/function_calling_filter_pipeline.py @@ -84,7 +84,11 @@ class Pipeline(FunctionCallingBlueprint): def __init__(self): super().__init__() - self.id = "my_tools_pipeline" + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + # self.id = "my_tools_pipeline" self.name = "My Tools Pipeline" self.valves = self.Valves( **{ diff --git a/examples/filters/langfuse_filter_pipeline.py b/examples/filters/langfuse_filter_pipeline.py index f88cad3..f126a70 100644 --- a/examples/filters/langfuse_filter_pipeline.py +++ b/examples/filters/langfuse_filter_pipeline.py @@ -40,10 +40,10 @@ class Pipeline: self.type = "filter" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "langfuse_filter_pipeline" + # self.id = "langfuse_filter_pipeline" self.name = "Langfuse Filter" # Initialize diff --git a/examples/filters/libretranslate_filter_pipeline.py b/examples/filters/libretranslate_filter_pipeline.py index 37a3547..39e8c5f 100644 --- a/examples/filters/libretranslate_filter_pipeline.py +++ b/examples/filters/libretranslate_filter_pipeline.py @@ -39,10 +39,10 @@ class Pipeline: self.type = "filter" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "libretranslate_filter_pipeline" + # self.id = "libretranslate_filter_pipeline" self.name = "LibreTranslate Filter" # Initialize diff --git a/examples/filters/rate_limit_filter_pipeline.py b/examples/filters/rate_limit_filter_pipeline.py index f03e18b..d1e8823 100644 --- a/examples/filters/rate_limit_filter_pipeline.py +++ b/examples/filters/rate_limit_filter_pipeline.py @@ -27,10 +27,11 @@ class Pipeline: # You can think of filter pipeline as a middleware that can be used to edit the form data before it is sent to the OpenAI API. self.type = "filter" - # Assign a unique identifier to the pipeline. + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "rate_limit_filter_pipeline" + # self.id = "rate_limit_filter_pipeline" self.name = "Rate Limit Filter" # Initialize rate limits diff --git a/examples/pipelines/integrations/applescript_pipeline.py b/examples/pipelines/integrations/applescript_pipeline.py index c4910a4..bc6266f 100644 --- a/examples/pipelines/integrations/applescript_pipeline.py +++ b/examples/pipelines/integrations/applescript_pipeline.py @@ -9,11 +9,10 @@ from subprocess import call class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - - self.id = "applescript_pipeline" + # self.id = "applescript_pipeline" self.name = "AppleScript Pipeline" pass diff --git a/examples/pipelines/integrations/python_code_pipeline.py b/examples/pipelines/integrations/python_code_pipeline.py index bfe432a..938d984 100644 --- a/examples/pipelines/integrations/python_code_pipeline.py +++ b/examples/pipelines/integrations/python_code_pipeline.py @@ -6,7 +6,10 @@ import subprocess class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - self.id = "python_code_pipeline" + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + # self.id = "python_code_pipeline" self.name = "Python Code Pipeline" pass diff --git a/examples/pipelines/integrations/wikipedia_pipeline.py b/examples/pipelines/integrations/wikipedia_pipeline.py index d430123..433a407 100644 --- a/examples/pipelines/integrations/wikipedia_pipeline.py +++ b/examples/pipelines/integrations/wikipedia_pipeline.py @@ -10,10 +10,11 @@ class Pipeline: pass def __init__(self): - # Assign a unique identifier to the pipeline. + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "wiki_pipeline" + # self.id = "wiki_pipeline" self.name = "Wikipedia Pipeline" # Initialize rate limits diff --git a/examples/pipelines/providers/azure_openai_pipeline.py b/examples/pipelines/providers/azure_openai_pipeline.py index 088d8a8..92ef3ad 100644 --- a/examples/pipelines/providers/azure_openai_pipeline.py +++ b/examples/pipelines/providers/azure_openai_pipeline.py @@ -16,10 +16,10 @@ class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "azure_openai_pipeline" + # self.id = "azure_openai_pipeline" self.name = "Azure OpenAI Pipeline" self.valves = self.Valves() pass diff --git a/examples/pipelines/providers/cohere_manifold_pipeline.py b/examples/pipelines/providers/cohere_manifold_pipeline.py index 20bc5ec..c99f8c3 100644 --- a/examples/pipelines/providers/cohere_manifold_pipeline.py +++ b/examples/pipelines/providers/cohere_manifold_pipeline.py @@ -24,7 +24,13 @@ class Pipeline: def __init__(self): self.type = "manifold" - self.id = "cohere" + + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + # self.id = "cohere" + self.name = "cohere/" self.valves = self.Valves(**{"COHERE_API_KEY": os.getenv("COHERE_API_KEY")}) diff --git a/examples/pipelines/providers/litellm_manifold_pipeline.py b/examples/pipelines/providers/litellm_manifold_pipeline.py index f12e93a..40c891c 100644 --- a/examples/pipelines/providers/litellm_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_manifold_pipeline.py @@ -25,10 +25,10 @@ class Pipeline: self.type = "manifold" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "litellm_manifold" + # self.id = "litellm_manifold" # Optionally, you can set the name of the manifold pipeline. self.name = "LiteLLM: " diff --git a/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py index f2bdeff..4fab306 100644 --- a/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py @@ -34,10 +34,10 @@ class Pipeline: self.type = "manifold" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "litellm_subprocess_manifold" + # self.id = "litellm_subprocess_manifold" # Optionally, you can set the name of the manifold pipeline. self.name = "LiteLLM: " diff --git a/examples/pipelines/providers/llama_cpp_pipeline.py b/examples/pipelines/providers/llama_cpp_pipeline.py index 1bb05a6..51692da 100644 --- a/examples/pipelines/providers/llama_cpp_pipeline.py +++ b/examples/pipelines/providers/llama_cpp_pipeline.py @@ -15,10 +15,10 @@ from schemas import OpenAIChatMessage class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "llama_cpp_pipeline" + # self.id = "llama_cpp_pipeline" self.name = "Llama C++ Pipeline" self.llm = None diff --git a/examples/pipelines/providers/mlx_pipeline.py b/examples/pipelines/providers/mlx_pipeline.py index 5ba6e92..3921677 100644 --- a/examples/pipelines/providers/mlx_pipeline.py +++ b/examples/pipelines/providers/mlx_pipeline.py @@ -21,10 +21,10 @@ from huggingface_hub import login class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "mlx_pipeline" + # self.id = "mlx_pipeline" self.name = "MLX Pipeline" self.host = os.getenv("MLX_HOST", "localhost") self.port = os.getenv("MLX_PORT", "8080") diff --git a/examples/pipelines/providers/ollama_manifold_pipeline.py b/examples/pipelines/providers/ollama_manifold_pipeline.py index a564113..d2e9fce 100644 --- a/examples/pipelines/providers/ollama_manifold_pipeline.py +++ b/examples/pipelines/providers/ollama_manifold_pipeline.py @@ -16,10 +16,10 @@ class Pipeline: self.type = "manifold" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "ollama_manifold" + # self.id = "ollama_manifold" # Optionally, you can set the name of the manifold pipeline. self.name = "Ollama: " diff --git a/examples/pipelines/providers/ollama_pipeline.py b/examples/pipelines/providers/ollama_pipeline.py index c7134cc..d2560d6 100644 --- a/examples/pipelines/providers/ollama_pipeline.py +++ b/examples/pipelines/providers/ollama_pipeline.py @@ -6,10 +6,10 @@ import requests class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "ollama_pipeline" + # self.id = "ollama_pipeline" self.name = "Ollama Pipeline" pass diff --git a/examples/pipelines/providers/openai_pipeline.py b/examples/pipelines/providers/openai_pipeline.py index e72f369..f56905c 100644 --- a/examples/pipelines/providers/openai_pipeline.py +++ b/examples/pipelines/providers/openai_pipeline.py @@ -6,10 +6,10 @@ import requests class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "openai_pipeline" + # self.id = "openai_pipeline" self.name = "OpenAI Pipeline" pass diff --git a/examples/scaffolds/example_pipeline_scaffold.py b/examples/scaffolds/example_pipeline_scaffold.py index c321f4b..11e4f0a 100644 --- a/examples/scaffolds/example_pipeline_scaffold.py +++ b/examples/scaffolds/example_pipeline_scaffold.py @@ -9,12 +9,13 @@ class Pipeline: def __init__(self): # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "pipeline_example" - self.name = "Pipeline Example" + # self.id = "pipeline_example" + # The name of the pipeline. + self.name = "Pipeline Example" pass async def on_startup(self): diff --git a/examples/scaffolds/filter_pipeline_scaffold.py b/examples/scaffolds/filter_pipeline_scaffold.py index 0e303c4..562ced3 100644 --- a/examples/scaffolds/filter_pipeline_scaffold.py +++ b/examples/scaffolds/filter_pipeline_scaffold.py @@ -33,10 +33,11 @@ class Pipeline: self.type = "filter" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "filter_pipeline" + # self.id = "filter_pipeline" + self.name = "Filter" self.valves = self.Valves(**{"pipelines": ["llama3:latest"]}) diff --git a/examples/scaffolds/function_calling_scaffold.py b/examples/scaffolds/function_calling_scaffold.py index 2c9f726..63940e6 100644 --- a/examples/scaffolds/function_calling_scaffold.py +++ b/examples/scaffolds/function_calling_scaffold.py @@ -17,7 +17,12 @@ class Pipeline(FunctionCallingBlueprint): def __init__(self): super().__init__() - self.id = "my_tools_pipeline" + + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + # self.id = "my_tools_pipeline" self.name = "My Tools Pipeline" self.valves = self.Valves( **{ diff --git a/examples/scaffolds/manifold_pipeline_scaffold.py b/examples/scaffolds/manifold_pipeline_scaffold.py index 80c64fd..13bfc66 100644 --- a/examples/scaffolds/manifold_pipeline_scaffold.py +++ b/examples/scaffolds/manifold_pipeline_scaffold.py @@ -10,10 +10,10 @@ class Pipeline: self.type = "manifold" # Optionally, you can set the id and name of the pipeline. - # Assign a unique identifier to the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - self.id = "manifold_pipeline" + # self.id = "manifold_pipeline" # Optionally, you can set the name of the manifold pipeline. self.name = "Manifold: " From 068be14a36cd48465c72c2ae67cb251ef8b214cc Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 15:06:35 -0700 Subject: [PATCH 34/51] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85e3533..325236a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ## 🚀 Why Choose Pipelines? - **Limitless Possibilities:** Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs. -- **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. +- **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. Only pipe-type pipelines are supported; filter types require clients with Pipelines support. - **Custom Hooks:** Build and integrate custom pipelines. ### Examples of What You Can Achieve: From b5bfe684f5842178dfb89bc464130a97807f50ed Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 15:08:13 -0700 Subject: [PATCH 35/51] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 325236a..bf93c44 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Welcome to **Pipelines**, an [Open WebUI](https://github.com/open-webui) initiat ## 🚀 Why Choose Pipelines? - **Limitless Possibilities:** Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs. -- **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. Only pipe-type pipelines are supported; filter types require clients with Pipelines support. +- **Seamless Integration:** Compatible with any UI/client supporting OpenAI API specs. (Only pipe-type pipelines are supported; filter types require clients with Pipelines support.) - **Custom Hooks:** Build and integrate custom pipelines. ### Examples of What You Can Achieve: From 73c340f4ebc97f7614c6476f8c7637eb221bf913 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 15:10:00 -0700 Subject: [PATCH 36/51] chore: comment --- examples/scaffolds/manifold_pipeline_scaffold.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/scaffolds/manifold_pipeline_scaffold.py b/examples/scaffolds/manifold_pipeline_scaffold.py index 13bfc66..623a027 100644 --- a/examples/scaffolds/manifold_pipeline_scaffold.py +++ b/examples/scaffolds/manifold_pipeline_scaffold.py @@ -17,6 +17,9 @@ class Pipeline: # Optionally, you can set the name of the manifold pipeline. self.name = "Manifold: " + + # Define pipelines that are available in this manifold pipeline. + # This is a list of dictionaries where each dictionary has an id and name. self.pipelines = [ { "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` From c19f52bd7b9a32cedbd7a97f241d73599bc609fd Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 15:14:06 -0700 Subject: [PATCH 37/51] enh: scaffolds --- examples/scaffolds/example_pipeline_scaffold.py | 4 ++++ examples/scaffolds/filter_pipeline_scaffold.py | 4 ++++ examples/scaffolds/manifold_pipeline_scaffold.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/examples/scaffolds/example_pipeline_scaffold.py b/examples/scaffolds/example_pipeline_scaffold.py index 11e4f0a..cb0ec11 100644 --- a/examples/scaffolds/example_pipeline_scaffold.py +++ b/examples/scaffolds/example_pipeline_scaffold.py @@ -56,6 +56,10 @@ class Pipeline: # This is where you can add your custom pipelines like RAG. print(f"pipe:{__name__}") + # If you'd like to check for title generation, you can add the following check + if body.get("title", False): + print("Title Generation Request") + print(messages) print(user_message) print(body) diff --git a/examples/scaffolds/filter_pipeline_scaffold.py b/examples/scaffolds/filter_pipeline_scaffold.py index 562ced3..cc08434 100644 --- a/examples/scaffolds/filter_pipeline_scaffold.py +++ b/examples/scaffolds/filter_pipeline_scaffold.py @@ -58,6 +58,10 @@ class Pipeline: # This filter is applied to the form data before it is sent to the OpenAI API. print(f"inlet:{__name__}") + # If you'd like to check for title generation, you can add the following check + if body.get("title", False): + print("Title Generation Request") + print(body) print(user) diff --git a/examples/scaffolds/manifold_pipeline_scaffold.py b/examples/scaffolds/manifold_pipeline_scaffold.py index 623a027..eaff91e 100644 --- a/examples/scaffolds/manifold_pipeline_scaffold.py +++ b/examples/scaffolds/manifold_pipeline_scaffold.py @@ -48,6 +48,10 @@ class Pipeline: # This is where you can add your custom pipelines like RAG. print(f"pipe:{__name__}") + # If you'd like to check for title generation, you can add the following check + if body.get("title", False): + print("Title Generation Request") + print(messages) print(user_message) print(body) From e024afc81ca3656ad176740ba2dcf6e6d409b30d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 16:56:35 -0700 Subject: [PATCH 38/51] feat: openai manifold --- .../providers/ollama_manifold_pipeline.py | 2 +- .../providers/openai_manifold_pipeline.py | 123 ++++++++++++++++++ main.py | 14 ++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 examples/pipelines/providers/openai_manifold_pipeline.py diff --git a/examples/pipelines/providers/ollama_manifold_pipeline.py b/examples/pipelines/providers/ollama_manifold_pipeline.py index d2e9fce..4393b3d 100644 --- a/examples/pipelines/providers/ollama_manifold_pipeline.py +++ b/examples/pipelines/providers/ollama_manifold_pipeline.py @@ -58,7 +58,7 @@ class Pipeline: print(f"Error: {e}") return [ { - "id": self.id, + "id": "error", "name": "Could not fetch models from Ollama, please update the URL in the valves.", }, ] diff --git a/examples/pipelines/providers/openai_manifold_pipeline.py b/examples/pipelines/providers/openai_manifold_pipeline.py new file mode 100644 index 0000000..0d667f5 --- /dev/null +++ b/examples/pipelines/providers/openai_manifold_pipeline.py @@ -0,0 +1,123 @@ +from typing import List, Union, Generator, Iterator +from schemas import OpenAIChatMessage +from pydantic import BaseModel + +import os +import requests + + +class Pipeline: + class Valves(BaseModel): + OPENAI_API_BASE_URL: str = "https://api.openai.com/v1" + OPENAI_API_KEY: str = "" + pass + + def __init__(self): + self.type = "manifold" + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + # self.id = "openai_pipeline" + self.name = "OpenAI: " + + self.valves = self.Valves( + **{ + "OPENAI_API_KEY": os.getenv( + "OPENAI_API_KEY", "your-openai-api-key-here" + ) + } + ) + + self.pipelines = self.get_openai_models() + pass + + async def on_startup(self): + # This function is called when the server is started. + print(f"on_startup:{__name__}") + pass + + async def on_shutdown(self): + # This function is called when the server is stopped. + print(f"on_shutdown:{__name__}") + pass + + async def on_valves_updated(self): + # This function is called when the valves are updated. + print(f"on_valves_updated:{__name__}") + self.pipelines = self.get_openai_models() + pass + + def get_openai_models(self): + if self.valves.OPENAI_API_KEY: + try: + headers = {} + headers["Authorization"] = f"Bearer {self.valves.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + r = requests.get( + f"{self.valves.OPENAI_API_BASE_URL}/models", headers=headers + ) + + models = r.json() + return [ + { + "id": model["id"], + "name": model["name"] if "name" in model else model["id"], + } + for model in models["data"] + if "gpt" in model["id"] + ] + + except Exception as e: + + print(f"Error: {e}") + return [ + { + "id": "error", + "name": "Could not fetch models from OpenAI, please update the API Key in the valves.", + }, + ] + else: + return [] + + def pipe( + self, user_message: str, model_id: str, messages: List[dict], body: dict + ) -> Union[str, Generator, Iterator]: + # This is where you can add your custom pipelines like RAG. + print(f"pipe:{__name__}") + + print(messages) + print(user_message) + + headers = {} + headers["Authorization"] = f"Bearer {self.valves.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + payload = {**body, "model": model_id} + + if "user" in payload: + del payload["user"] + if "chat_id" in payload: + del payload["chat_id"] + if "title" in payload: + del payload["title"] + + print(payload) + + try: + r = requests.post( + url=f"{self.valves.OPENAI_API_BASE_URL}/chat/completions", + json=payload, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + if body["stream"]: + return r.iter_lines() + else: + return r.json() + except Exception as e: + return f"Error: {e}" diff --git a/main.py b/main.py index 6e5c23b..0bb917f 100644 --- a/main.py +++ b/main.py @@ -506,6 +506,13 @@ async def filter_inlet(pipeline_id: str, form_data: FilterForm): detail=f"Filter {pipeline_id} not found", ) + try: + pipeline = app.state.PIPELINES[form_data.body["model"]] + if pipeline["type"] == "manifold": + pipeline_id = pipeline_id.split(".")[0] + except: + pass + pipeline = PIPELINE_MODULES[pipeline_id] try: @@ -531,6 +538,13 @@ async def filter_outlet(pipeline_id: str, form_data: FilterForm): detail=f"Filter {pipeline_id} not found", ) + try: + pipeline = app.state.PIPELINES[form_data.body["model"]] + if pipeline["type"] == "manifold": + pipeline_id = pipeline_id.split(".")[0] + except: + pass + pipeline = PIPELINE_MODULES[pipeline_id] try: From c2f5200906c570562ba0841594f7070f1985b59c Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 16:58:28 -0700 Subject: [PATCH 39/51] fix: openai pipeline --- .../pipelines/providers/openai_pipeline.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/examples/pipelines/providers/openai_pipeline.py b/examples/pipelines/providers/openai_pipeline.py index f56905c..b374227 100644 --- a/examples/pipelines/providers/openai_pipeline.py +++ b/examples/pipelines/providers/openai_pipeline.py @@ -1,9 +1,15 @@ from typing import List, Union, Generator, Iterator from schemas import OpenAIChatMessage +from pydantic import BaseModel +import os import requests class Pipeline: + class Valves(BaseModel): + OPENAI_API_KEY: str = "" + pass + def __init__(self): # Optionally, you can set the id and name of the pipeline. # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. @@ -11,6 +17,13 @@ class Pipeline: # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. # self.id = "openai_pipeline" self.name = "OpenAI Pipeline" + self.valves = self.Valves( + **{ + "OPENAI_API_KEY": os.getenv( + "OPENAI_API_KEY", "your-openai-api-key-here" + ) + } + ) pass async def on_startup(self): @@ -39,10 +52,21 @@ class Pipeline: headers["Authorization"] = f"Bearer {OPENAI_API_KEY}" headers["Content-Type"] = "application/json" + payload = {**body, "model": MODEL} + + if "user" in payload: + del payload["user"] + if "chat_id" in payload: + del payload["chat_id"] + if "title" in payload: + del payload["title"] + + print(payload) + try: r = requests.post( url="https://api.openai.com/v1/chat/completions", - json={**body, "model": MODEL}, + json=payload, headers=headers, stream=True, ) From 5b1d61f8f9b4fecd603e3298062e3f57b52ad73a Mon Sep 17 00:00:00 2001 From: weizhou88 <117541284+weizhou88@users.noreply.github.com> Date: Mon, 3 Jun 2024 07:58:34 +0800 Subject: [PATCH 40/51] Update azure_openai_pipeline.py --- examples/pipelines/providers/azure_openai_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pipelines/providers/azure_openai_pipeline.py b/examples/pipelines/providers/azure_openai_pipeline.py index 92ef3ad..4c7e9e3 100644 --- a/examples/pipelines/providers/azure_openai_pipeline.py +++ b/examples/pipelines/providers/azure_openai_pipeline.py @@ -48,7 +48,7 @@ class Pipeline: "Content-Type": "application/json", } - url = f"{self.valves.AZURE_OPENAI_ENDPOINT}/openai/deployments/{self.valves.DEPLOYMENT_NAME}/chat/completions?api-version={self.valves.API_VERSION}" + url = f"{self.valves.AZURE_OPENAI_ENDPOINT}/openai/deployments/{self.valves.DEPLOYMENT_NAME}/completions?api-version={self.valves.API_VERSION}" try: r = requests.post( From 0f516bd95e68765c7c919043b2321fe44a3de180 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 16:59:43 -0700 Subject: [PATCH 41/51] fix: anthropic --- .../pipelines/providers/anthropic_manifold_pipeline.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/pipelines/providers/anthropic_manifold_pipeline.py b/examples/pipelines/providers/anthropic_manifold_pipeline.py index 1cfcb14..34965e8 100644 --- a/examples/pipelines/providers/anthropic_manifold_pipeline.py +++ b/examples/pipelines/providers/anthropic_manifold_pipeline.py @@ -63,6 +63,13 @@ class Pipeline: self, user_message: str, model_id: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: try: + if "user" in body: + del body["user"] + if "chat_id" in body: + del body["chat_id"] + if "title" in body: + del body["title"] + if body.get("stream", False): return self.stream_response(model_id, messages, body) else: From b882dd79ff9b6a21f362a8a5818946573ada1134 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 17:00:44 -0700 Subject: [PATCH 42/51] fix: litellm pipelines --- examples/pipelines/providers/litellm_manifold_pipeline.py | 2 +- .../pipelines/providers/litellm_subprocess_manifold_pipeline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pipelines/providers/litellm_manifold_pipeline.py b/examples/pipelines/providers/litellm_manifold_pipeline.py index 40c891c..0b6771f 100644 --- a/examples/pipelines/providers/litellm_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_manifold_pipeline.py @@ -70,7 +70,7 @@ class Pipeline: print(f"Error: {e}") return [ { - "id": self.id, + "id": "error", "name": "Could not fetch models from LiteLLM, please update the URL in the valves.", }, ] diff --git a/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py index 4fab306..4213c36 100644 --- a/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py @@ -173,7 +173,7 @@ class Pipeline: print(f"Error: {e}") return [ { - "id": self.id, + "id": "error", "name": "Could not fetch models from LiteLLM, please update the URL in the valves.", }, ] From d0cf5b7bcbd1a7934050e525b118aedf3e3a1449 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 18:07:04 -0700 Subject: [PATCH 43/51] fix: azure --- examples/pipelines/providers/azure_openai_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pipelines/providers/azure_openai_pipeline.py b/examples/pipelines/providers/azure_openai_pipeline.py index 4c7e9e3..92ef3ad 100644 --- a/examples/pipelines/providers/azure_openai_pipeline.py +++ b/examples/pipelines/providers/azure_openai_pipeline.py @@ -48,7 +48,7 @@ class Pipeline: "Content-Type": "application/json", } - url = f"{self.valves.AZURE_OPENAI_ENDPOINT}/openai/deployments/{self.valves.DEPLOYMENT_NAME}/completions?api-version={self.valves.API_VERSION}" + url = f"{self.valves.AZURE_OPENAI_ENDPOINT}/openai/deployments/{self.valves.DEPLOYMENT_NAME}/chat/completions?api-version={self.valves.API_VERSION}" try: r = requests.post( From 72ed9c537c270a6434d044e4a441cf2cc7f8dc58 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 2 Jun 2024 18:49:26 -0700 Subject: [PATCH 44/51] feat: move failing pipelines to failed dir --- main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.py b/main.py index 0bb917f..04626fd 100644 --- a/main.py +++ b/main.py @@ -118,6 +118,14 @@ async def load_module_from_path(module_name, module_path): raise Exception("No Pipeline class found") except Exception as e: print(f"Error loading module: {module_name}") + + # Move the file to the error folder + failed_pipelines_folder = os.path.join(PIPELINES_DIR, "failed") + if not os.path.exists(failed_pipelines_folder): + os.makedirs(failed_pipelines_folder) + + failed_file_path = os.path.join(failed_pipelines_folder, f"{module_name}.py") + os.rename(module_path, failed_file_path) print(e) return None From 84cfa62cd4a0a7656ad33214c0d9ef4b0fc18889 Mon Sep 17 00:00:00 2001 From: Justin Hayes Date: Sun, 2 Jun 2024 22:08:49 -0400 Subject: [PATCH 45/51] Fix id --- examples/pipelines/providers/cohere_manifold_pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/pipelines/providers/cohere_manifold_pipeline.py b/examples/pipelines/providers/cohere_manifold_pipeline.py index c99f8c3..4bdd7fe 100644 --- a/examples/pipelines/providers/cohere_manifold_pipeline.py +++ b/examples/pipelines/providers/cohere_manifold_pipeline.py @@ -29,7 +29,8 @@ class Pipeline: # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. # The identifier must be unique across all pipelines. # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. - # self.id = "cohere" + + self.id = "cohere" self.name = "cohere/" From 17bc338c4a23313cbbd40489e4aac71fc8988f52 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 3 Jun 2024 13:24:57 -0700 Subject: [PATCH 46/51] enh: litellm manifold Co-Authored-By: Artur Zdolinski <15941777+azdolinski@users.noreply.github.com> --- .../providers/litellm_manifold_pipeline.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/examples/pipelines/providers/litellm_manifold_pipeline.py b/examples/pipelines/providers/litellm_manifold_pipeline.py index 0b6771f..80fdf11 100644 --- a/examples/pipelines/providers/litellm_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_manifold_pipeline.py @@ -11,12 +11,15 @@ from typing import List, Union, Generator, Iterator from schemas import OpenAIChatMessage from pydantic import BaseModel import requests +import os class Pipeline: class Valves(BaseModel): - LITELLM_BASE_URL: str + LITELLM_BASE_URL: str = "" + LITELLM_API_KEY: str = "" + LITELLM_PIPELINE_DEBUG: bool = False def __init__(self): # You can also set the pipelines that are available in this pipeline. @@ -34,7 +37,15 @@ class Pipeline: self.name = "LiteLLM: " # Initialize rate limits - self.valves = self.Valves(**{"LITELLM_BASE_URL": "http://localhost:4001"}) + self.valves = self.Valves( + **{ + "LITELLM_BASE_URL": os.getenv( + "LITELLM_BASE_URL", "http://localhost:4001" + ), + "LITELLM_API_KEY": os.getenv("LITELLM_API_KEY", "your-api-key-here"), + "LITELLM_PIPELINE_DEBUG": os.getenv("LITELLM_PIPELINE_DEBUG", False), + } + ) self.pipelines = [] pass @@ -55,9 +66,16 @@ class Pipeline: pass def get_litellm_models(self): + + headers = {} + if self.valves.LITELLM_API_KEY: + headers["Authorization"] = f"Bearer {self.valves.LITELLM_API_KEY}" + if self.valves.LITELLM_BASE_URL: try: - r = requests.get(f"{self.valves.LITELLM_BASE_URL}/v1/models") + r = requests.get( + f"{self.valves.LITELLM_BASE_URL}/v1/models", headers=headers + ) models = r.json() return [ { @@ -86,10 +104,20 @@ class Pipeline: print(f"# Message: {user_message}") print("######################################") + headers = {} + if self.valves.LITELLM_API_KEY: + headers["Authorization"] = f"Bearer {self.valves.LITELLM_API_KEY}" + try: + payload = {**body, "model": model_id, "user_id": body["user"]["id"]} + payload.pop("chat_id", None) + payload.pop("user", None) + payload.pop("title", None) + r = requests.post( url=f"{self.valves.LITELLM_BASE_URL}/v1/chat/completions", - json={**body, "model": model_id, "user_id": body["user"]["id"]}, + json=payload, + headers=headers, stream=True, ) From ecd7f8e5ad964112fc3b0ff7d9630d852fa8353f Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 3 Jun 2024 18:02:21 -0700 Subject: [PATCH 47/51] fix: litellm --- examples/pipelines/providers/litellm_manifold_pipeline.py | 2 +- .../pipelines/providers/litellm_subprocess_manifold_pipeline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pipelines/providers/litellm_manifold_pipeline.py b/examples/pipelines/providers/litellm_manifold_pipeline.py index 80fdf11..79609eb 100644 --- a/examples/pipelines/providers/litellm_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_manifold_pipeline.py @@ -109,7 +109,7 @@ class Pipeline: headers["Authorization"] = f"Bearer {self.valves.LITELLM_API_KEY}" try: - payload = {**body, "model": model_id, "user_id": body["user"]["id"]} + payload = {**body, "model": model_id, "user": body["user"]["id"]} payload.pop("chat_id", None) payload.pop("user", None) payload.pop("title", None) diff --git a/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py index 4213c36..99b778a 100644 --- a/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py +++ b/examples/pipelines/providers/litellm_subprocess_manifold_pipeline.py @@ -197,7 +197,7 @@ class Pipeline: try: r = requests.post( url=f"http://{self.valves.LITELLM_PROXY_HOST}:{self.valves.LITELLM_PROXY_PORT}/v1/chat/completions", - json={**body, "model": model_id, "user_id": body["user"]["id"]}, + json={**body, "model": model_id, "user": body["user"]["id"]}, stream=True, ) From 3f7a44d4b84a56b4ddcb7be6096248412a87272a Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Tue, 4 Jun 2024 10:05:58 -0700 Subject: [PATCH 48/51] fix: anthropic --- examples/pipelines/providers/anthropic_manifold_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pipelines/providers/anthropic_manifold_pipeline.py b/examples/pipelines/providers/anthropic_manifold_pipeline.py index 34965e8..7ba9395 100644 --- a/examples/pipelines/providers/anthropic_manifold_pipeline.py +++ b/examples/pipelines/providers/anthropic_manifold_pipeline.py @@ -28,7 +28,7 @@ class Pipeline: self.name = "anthropic/" self.valves = self.Valves( - **{"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY")} + **{"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", "your-api-key-here")} ) self.client = Anthropic(api_key=self.valves.ANTHROPIC_API_KEY) From 5743db5d0330fcc151b0fe8311297cb1bf195b3f Mon Sep 17 00:00:00 2001 From: Justin Hayes Date: Tue, 4 Jun 2024 13:16:14 -0400 Subject: [PATCH 49/51] Cohere envvar fix --- examples/pipelines/providers/cohere_manifold_pipeline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/pipelines/providers/cohere_manifold_pipeline.py b/examples/pipelines/providers/cohere_manifold_pipeline.py index 4bdd7fe..61fcf8b 100644 --- a/examples/pipelines/providers/cohere_manifold_pipeline.py +++ b/examples/pipelines/providers/cohere_manifold_pipeline.py @@ -34,7 +34,9 @@ class Pipeline: self.name = "cohere/" - self.valves = self.Valves(**{"COHERE_API_KEY": os.getenv("COHERE_API_KEY")}) + self.valves = self.Valves( + **{"COHERE_API_KEY": os.getenv("COHERE_API_KEY", "your-api-key-here")} + ) self.pipelines = self.get_cohere_models() From 5907d13eccd4cae710df64aa40d506b7271fcf6b Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 4 Jun 2024 21:05:31 +0300 Subject: [PATCH 50/51] doc: mention PIPELINES_DIR in readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf93c44..95f78b7 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,11 @@ Once the server is running, set the OpenAI URL on your client to the Pipelines U The `/pipelines` directory is the core of your setup. Add new modules, customize existing ones, and manage your workflows here. All the pipelines in the `/pipelines` directory will be **automatically loaded** when the server launches. +You can change this directory from `/pipelines` to another location using the `PIPELINES_DIR` env variable. + ### Integration Examples -Find various integration examples in the `/pipelines/examples` directory. These examples show how to integrate different functionalities, providing a foundation for building your own custom pipelines. +Find various integration examples in the `/examples` directory. These examples show how to integrate different functionalities, providing a foundation for building your own custom pipelines. ## 🎉 Work in Progress From 47a7c82366a3554307916b846774c2edd6b36f71 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 5 Jun 2024 09:04:43 +0300 Subject: [PATCH 51/51] feat: groq manifold pipeline --- .../providers/groq_manifold_pipeline.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100755 examples/pipelines/providers/groq_manifold_pipeline.py diff --git a/examples/pipelines/providers/groq_manifold_pipeline.py b/examples/pipelines/providers/groq_manifold_pipeline.py new file mode 100755 index 0000000..717f738 --- /dev/null +++ b/examples/pipelines/providers/groq_manifold_pipeline.py @@ -0,0 +1,122 @@ +from typing import List, Union, Generator, Iterator +from schemas import OpenAIChatMessage +from pydantic import BaseModel + +import os +import requests + + +class Pipeline: + class Valves(BaseModel): + GROQ_API_BASE_URL: str = "https://api.groq.com/openai/v1" + GROQ_API_KEY: str = "" + pass + + def __init__(self): + self.type = "manifold" + # Optionally, you can set the id and name of the pipeline. + # Best practice is to not specify the id so that it can be automatically inferred from the filename, so that users can install multiple versions of the same pipeline. + # The identifier must be unique across all pipelines. + # The identifier must be an alphanumeric string that can include underscores or hyphens. It cannot contain spaces, special characters, slashes, or backslashes. + self.id = "groq" + self.name = "Groq: " + + self.valves = self.Valves( + **{ + "GROQ_API_KEY": os.getenv( + "GROQ_API_KEY", "your-groq-api-key-here" + ) + } + ) + + self.pipelines = self.get_models() + pass + + async def on_startup(self): + # This function is called when the server is started. + print(f"on_startup:{__name__}") + pass + + async def on_shutdown(self): + # This function is called when the server is stopped. + print(f"on_shutdown:{__name__}") + pass + + async def on_valves_updated(self): + # This function is called when the valves are updated. + print(f"on_valves_updated:{__name__}") + self.pipelines = self.get_models() + pass + + def get_models(self): + if self.valves.GROQ_API_KEY: + try: + headers = {} + headers["Authorization"] = f"Bearer {self.valves.GROQ_API_KEY}" + headers["Content-Type"] = "application/json" + + r = requests.get( + f"{self.valves.GROQ_API_BASE_URL}/models", headers=headers + ) + + models = r.json() + return [ + { + "id": model["id"], + "name": model["name"] if "name" in model else model["id"], + } + for model in models["data"] + ] + + except Exception as e: + + print(f"Error: {e}") + return [ + { + "id": "error", + "name": "Could not fetch models from Groq, please update the API Key in the valves.", + }, + ] + else: + return [] + + def pipe( + self, user_message: str, model_id: str, messages: List[dict], body: dict + ) -> Union[str, Generator, Iterator]: + # This is where you can add your custom pipelines like RAG. + print(f"pipe:{__name__}") + + print(messages) + print(user_message) + + headers = {} + headers["Authorization"] = f"Bearer {self.valves.GROQ_API_KEY}" + headers["Content-Type"] = "application/json" + + payload = {**body, "model": model_id} + + if "user" in payload: + del payload["user"] + if "chat_id" in payload: + del payload["chat_id"] + if "title" in payload: + del payload["title"] + + print(payload) + + try: + r = requests.post( + url=f"{self.valves.GROQ_API_BASE_URL}/chat/completions", + json=payload, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + if body["stream"]: + return r.iter_lines() + else: + return r.json() + except Exception as e: + return f"Error: {e}" \ No newline at end of file

- Pipelines Workflow + Pipelines Workflow