From 72a119f4db2e5a6de8f0d38ead3f81460a17654a Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 22 May 2024 13:33:44 -0700 Subject: [PATCH 01/20] refac --- main.py | 5 +++-- pipelines/examples/applescript_pipeline.py | 2 +- pipelines/examples/azure_openai_pipeline.py | 2 +- pipelines/examples/haystack_pipeline.py | 2 +- pipelines/examples/llama_cpp_pipeline.py | 2 +- pipelines/examples/llamaindex_ollama_github_pipeline.py | 2 +- pipelines/examples/llamaindex_ollama_pipeline.py | 2 +- pipelines/examples/llamaindex_pipeline.py | 2 +- pipelines/examples/mlx_pipeline.py | 2 +- pipelines/examples/ollama_pipeline.py | 2 +- pipelines/examples/openai_pipeline.py | 2 +- pipelines/examples/pipeline_example.py | 2 +- pipelines/examples/python_code_pipeline.py | 2 +- pipelines/ollama_pipeline.py | 2 +- schemas.py | 2 +- utils.py | 2 +- 16 files changed, 18 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index 63234e7..aef7888 100644 --- a/main.py +++ b/main.py @@ -117,6 +117,7 @@ async def get_models(): @app.post("/v1/chat/completions") async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): user_message = get_last_user_message(form_data.messages) + messages = [message.model_dump() for message in form_data.messages] if form_data.model not in app.state.PIPELINES: return HTTPException( @@ -133,7 +134,7 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): def stream_content(): res = get_response( user_message, - messages=form_data.messages, + messages=messages, body=form_data.model_dump(), ) @@ -186,7 +187,7 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): else: res = get_response( user_message, - messages=form_data.messages, + messages=messages, body=form_data.model_dump(), ) logging.info(f"stream:false:{res}") diff --git a/pipelines/examples/applescript_pipeline.py b/pipelines/examples/applescript_pipeline.py index d2c28f6..3453ddc 100644 --- a/pipelines/examples/applescript_pipeline.py +++ b/pipelines/examples/applescript_pipeline.py @@ -25,7 +25,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/azure_openai_pipeline.py b/pipelines/examples/azure_openai_pipeline.py index 0052760..6b746d0 100644 --- a/pipelines/examples/azure_openai_pipeline.py +++ b/pipelines/examples/azure_openai_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/haystack_pipeline.py b/pipelines/examples/haystack_pipeline.py index cea62ff..650a771 100644 --- a/pipelines/examples/haystack_pipeline.py +++ b/pipelines/examples/haystack_pipeline.py @@ -79,7 +79,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/llama_cpp_pipeline.py b/pipelines/examples/llama_cpp_pipeline.py index 2032558..c555993 100644 --- a/pipelines/examples/llama_cpp_pipeline.py +++ b/pipelines/examples/llama_cpp_pipeline.py @@ -30,7 +30,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/llamaindex_ollama_github_pipeline.py b/pipelines/examples/llamaindex_ollama_github_pipeline.py index 17d1461..2f091d6 100644 --- a/pipelines/examples/llamaindex_ollama_github_pipeline.py +++ b/pipelines/examples/llamaindex_ollama_github_pipeline.py @@ -70,7 +70,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/llamaindex_ollama_pipeline.py b/pipelines/examples/llamaindex_ollama_pipeline.py index 089c499..19ed721 100644 --- a/pipelines/examples/llamaindex_ollama_pipeline.py +++ b/pipelines/examples/llamaindex_ollama_pipeline.py @@ -30,7 +30,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/llamaindex_pipeline.py b/pipelines/examples/llamaindex_pipeline.py index 8dbce35..195e3f8 100644 --- a/pipelines/examples/llamaindex_pipeline.py +++ b/pipelines/examples/llamaindex_pipeline.py @@ -25,7 +25,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/mlx_pipeline.py b/pipelines/examples/mlx_pipeline.py index 71faa4f..8487d8e 100644 --- a/pipelines/examples/mlx_pipeline.py +++ b/pipelines/examples/mlx_pipeline.py @@ -73,7 +73,7 @@ class Pipeline: print(f"Failed to terminate subprocess: {e}") def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/ollama_pipeline.py b/pipelines/examples/ollama_pipeline.py index 437461d..876380a 100644 --- a/pipelines/examples/ollama_pipeline.py +++ b/pipelines/examples/ollama_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/openai_pipeline.py b/pipelines/examples/openai_pipeline.py index 1c712e4..f4273e7 100644 --- a/pipelines/examples/openai_pipeline.py +++ b/pipelines/examples/openai_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/pipeline_example.py b/pipelines/examples/pipeline_example.py index a0d1f95..1015ba8 100644 --- a/pipelines/examples/pipeline_example.py +++ b/pipelines/examples/pipeline_example.py @@ -20,7 +20,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/examples/python_code_pipeline.py b/pipelines/examples/python_code_pipeline.py index cfe1408..8d3aa20 100644 --- a/pipelines/examples/python_code_pipeline.py +++ b/pipelines/examples/python_code_pipeline.py @@ -31,7 +31,7 @@ class Pipeline: return e.output.strip(), e.returncode def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/pipelines/ollama_pipeline.py b/pipelines/ollama_pipeline.py index 437461d..876380a 100644 --- a/pipelines/ollama_pipeline.py +++ b/pipelines/ollama_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[OpenAIChatMessage], body: dict + self, user_message: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom pipelines like RAG.' print(f"get_response:{__name__}") diff --git a/schemas.py b/schemas.py index 6f90d68..5cad1e6 100644 --- a/schemas.py +++ b/schemas.py @@ -12,6 +12,6 @@ class OpenAIChatMessage(BaseModel): class OpenAIChatCompletionForm(BaseModel): stream: bool = True model: str - messages: List[OpenAIChatMessage] + messages: List[dict] model_config = ConfigDict(extra="allow") diff --git a/utils.py b/utils.py index 97b22fc..a50cddf 100644 --- a/utils.py +++ b/utils.py @@ -22,7 +22,7 @@ def stream_message_template(model: str, message: str): } -def get_last_user_message(messages: List[OpenAIChatMessage]) -> str: +def get_last_user_message(messages: List[dict]) -> str: for message in reversed(messages): if message.role == "user": return message.content From 2a25c2c9dc7f5b67c9a0c478e3ee017bee3b36fd Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 22 May 2024 13:34:04 -0700 Subject: [PATCH 02/20] fix --- schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas.py b/schemas.py index 5cad1e6..6f90d68 100644 --- a/schemas.py +++ b/schemas.py @@ -12,6 +12,6 @@ class OpenAIChatMessage(BaseModel): class OpenAIChatCompletionForm(BaseModel): stream: bool = True model: str - messages: List[dict] + messages: List[OpenAIChatMessage] model_config = ConfigDict(extra="allow") From b7331433afd327fa9673c9538c96c2107752ce60 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 22 May 2024 13:35:01 -0700 Subject: [PATCH 03/20] fix --- pipelines/examples/llama_cpp_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/examples/llama_cpp_pipeline.py b/pipelines/examples/llama_cpp_pipeline.py index c555993..47bf630 100644 --- a/pipelines/examples/llama_cpp_pipeline.py +++ b/pipelines/examples/llama_cpp_pipeline.py @@ -40,7 +40,7 @@ class Pipeline: print(body) response = self.llm.create_chat_completion_openai_v1( - messages=[message.model_dump() for message in messages], + messages=messages, stream=body["stream"], ) From 1b3a8681237b2f617b49a8f051750f7a7080c730 Mon Sep 17 00:00:00 2001 From: silentoplayz <50341825+silentoplayz@users.noreply.github.com> Date: Fri, 24 May 2024 16:46:23 -0400 Subject: [PATCH 04/20] Added start.bat file for Windows --- start.bat | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 start.bat diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..05c5795 --- /dev/null +++ b/start.bat @@ -0,0 +1,5 @@ +@echo off +set PORT=9099 +set HOST=0.0.0.0 + +uvicorn main:app --host %HOST% --port %PORT% --forwarded-allow-ips '*' \ No newline at end of file From eae9bf4a5e85071eb10a9ea49a614668cb9c8072 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 00:45:28 -0700 Subject: [PATCH 05/20] Delete readme.md --- readme.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 readme.md diff --git a/readme.md b/readme.md deleted file mode 100644 index 80d2f6e..0000000 --- a/readme.md +++ /dev/null @@ -1 +0,0 @@ -# OpenAI API Compatible Plugin Server From 4f2110fbec4c58b479975bd9dc36bdd59b01f3ed Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 00:52:53 -0700 Subject: [PATCH 06/20] Create README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..87de8e8 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Pipelines: OpenAI API Compatible Plugin Framework 🌟 + +Welcome to **Pipelines**, Open WebUI initiative that brings modular, customizable workflows to any UI client supporting OpenAI API specs – and much more! Dive into a world where you can effortlessly extend functionalities, integrate unique logic, and create dynamic agentic workflows, all with a few lines of code. + +## πŸš€ Why Pipelines? + +- **Seamless Integration:** Compatible with any UI/client that supports OpenAI API specs. +- **Endless Possibilities:** Got a specific need? Pipelines make it easy to add your custom logic and functionalities. Integrate any Python library, from AI agents via libraries like CrewAI to API calls for home automation – the sky's the limit! +- **Custom Hooks:** Build and integrate custom RAG (Retrospective Agent Guidance) pipelines and more. + +## πŸ”§ How It Works + +Integrating Pipelines with any OpenAI API-compatible UI client is a breeze. Simply launch your Pipelines instance and set the OpenAI URL on your client to the Pipelines URL. That's it! You're now ready to leverage any Python library, whether you want an agent to manage your home or need a custom pipeline for your enterprise workflow. + +## πŸŽ‰ Work in Progress + +We’re continuously evolving! We'd love to hear your feedback and understand which hooks and features would best suit your use case. Feel free to reach out and become a part of our Open WebUI community! + +Our vision is to push **Pipelines** to become the ultimate plugin framework for our AI interface, **Open-WebUI**. Imagine **Open-WebUI** as the WordPress of AI interfaces, with **Pipelines** being its diverse range of plugins. Join us on this exciting journey! 🌍 From 2766778fd9b1e7456166336f17e35633243cadce Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 00:53:32 -0700 Subject: [PATCH 07/20] fix: ai not taking over anytime soon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87de8e8..d15e800 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Welcome to **Pipelines**, Open WebUI initiative that brings modular, customizabl - **Seamless Integration:** Compatible with any UI/client that supports OpenAI API specs. - **Endless Possibilities:** Got a specific need? Pipelines make it easy to add your custom logic and functionalities. Integrate any Python library, from AI agents via libraries like CrewAI to API calls for home automation – the sky's the limit! -- **Custom Hooks:** Build and integrate custom RAG (Retrospective Agent Guidance) pipelines and more. +- **Custom Hooks:** Build and integrate custom RAG pipelines and more. ## πŸ”§ How It Works From 035209690b60f2acd7e60d2f2aa6edaed6a2c8f1 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 00:57:51 -0700 Subject: [PATCH 08/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d15e800..eb3b087 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Pipelines: OpenAI API Compatible Plugin Framework 🌟 +# Pipelines: UI-Agnostic OpenAI API Compatible Plugin Framework Welcome to **Pipelines**, Open WebUI initiative that brings modular, customizable workflows to any UI client supporting OpenAI API specs – and much more! Dive into a world where you can effortlessly extend functionalities, integrate unique logic, and create dynamic agentic workflows, all with a few lines of code. From aa2b4df7d889d74c7e33c048dff3adc1b3e38ed7 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 02:59:35 -0700 Subject: [PATCH 09/20] doc --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb3b087..adfb13f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pipelines: UI-Agnostic OpenAI API Compatible Plugin Framework -Welcome to **Pipelines**, Open WebUI initiative that brings modular, customizable workflows to any UI client supporting OpenAI API specs – and much more! Dive into a world where you can effortlessly extend functionalities, integrate unique logic, and create dynamic agentic workflows, all with a few lines of code. +Welcome to **Pipelines**, [Open WebUI](https://github.com/open-webui) initiative that brings modular, customizable workflows to any UI client supporting OpenAI API specs – and much more! Dive into a world where you can effortlessly extend functionalities, integrate unique logic, and create dynamic agentic workflows, all with a few lines of code. ## πŸš€ Why Pipelines? @@ -16,4 +16,4 @@ Integrating Pipelines with any OpenAI API-compatible UI client is a breeze. Simp We’re continuously evolving! We'd love to hear your feedback and understand which hooks and features would best suit your use case. Feel free to reach out and become a part of our Open WebUI community! -Our vision is to push **Pipelines** to become the ultimate plugin framework for our AI interface, **Open-WebUI**. Imagine **Open-WebUI** as the WordPress of AI interfaces, with **Pipelines** being its diverse range of plugins. Join us on this exciting journey! 🌍 +Our vision is to push **Pipelines** to become the ultimate plugin framework for our AI interface, **Open WebUI**. Imagine **Open WebUI** as the WordPress of AI interfaces, with **Pipelines** being its diverse range of plugins. Join us on this exciting journey! 🌍 From ebd3c3063d4b7e71b1587411044195f585867512 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 03:00:17 -0700 Subject: [PATCH 10/20] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index adfb13f..031b76b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Pipelines: UI-Agnostic OpenAI API Compatible Plugin Framework +# Pipelines: UI-Agnostic OpenAI API Plugin Framework Welcome to **Pipelines**, [Open WebUI](https://github.com/open-webui) initiative that brings modular, customizable workflows to any UI client supporting OpenAI API specs – and much more! Dive into a world where you can effortlessly extend functionalities, integrate unique logic, and create dynamic agentic workflows, all with a few lines of code. From eff0a968c785779bb75296925cdb4b6e5fbfe898 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:04:44 -0700 Subject: [PATCH 11/20] feat: manifold --- main.py | 67 ++++++++++++------- pipelines/examples/applescript_pipeline.py | 2 +- pipelines/examples/azure_openai_pipeline.py | 2 +- pipelines/examples/haystack_pipeline.py | 2 +- pipelines/examples/llama_cpp_pipeline.py | 2 +- .../llamaindex_ollama_github_pipeline.py | 2 +- .../examples/llamaindex_ollama_pipeline.py | 2 +- pipelines/examples/llamaindex_pipeline.py | 2 +- pipelines/examples/manifold_pipeline.py | 43 ++++++++++++ pipelines/examples/mlx_pipeline.py | 2 +- pipelines/examples/ollama_pipeline.py | 2 +- pipelines/examples/openai_pipeline.py | 2 +- pipelines/examples/pipeline_example.py | 2 +- pipelines/examples/python_code_pipeline.py | 2 +- pipelines/manifold_pipeline.py | 45 +++++++++++++ pipelines/ollama_pipeline.py | 2 +- 16 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 pipelines/examples/manifold_pipeline.py create mode 100644 pipelines/manifold_pipeline.py diff --git a/main.py b/main.py index aef7888..47f61b5 100644 --- a/main.py +++ b/main.py @@ -25,28 +25,46 @@ from concurrent.futures import ThreadPoolExecutor PIPELINES = {} -def load_modules_from_directory(directory): - for filename in os.listdir(directory): - if filename.endswith(".py"): - module_name = filename[:-3] # Remove the .py extension - module_path = os.path.join(directory, filename) - spec = importlib.util.spec_from_file_location(module_name, module_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - yield module +def on_startup(): + def load_modules_from_directory(directory): + for filename in os.listdir(directory): + if filename.endswith(".py"): + module_name = filename[:-3] # Remove the .py extension + module_path = os.path.join(directory, filename) + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + yield module + + for loaded_module in load_modules_from_directory("./pipelines"): + # Do something with the loaded module + logging.info("Loaded:", loaded_module.__name__) + + pipeline = loaded_module.Pipeline() + + pipeline_id = pipeline.id if hasattr(pipeline, "id") else loaded_module.__name__ + + if hasattr(pipeline, "manifold") and pipeline.manifold: + for p in pipeline.pipelines: + manifold_pipeline_id = f'{pipeline_id}.{p["id"]}' + PIPELINES[manifold_pipeline_id] = { + "module": pipeline, + "id": manifold_pipeline_id, + "name": p["name"], + } + else: + PIPELINES[loaded_module.__name__] = { + "module": pipeline, + "id": pipeline_id, + "name": ( + pipeline.name + if hasattr(pipeline, "name") + else loaded_module.__name__ + ), + } -for loaded_module in load_modules_from_directory("./pipelines"): - # Do something with the loaded module - logging.info("Loaded:", loaded_module.__name__) - - pipeline = loaded_module.Pipeline() - - PIPELINES[loaded_module.__name__] = { - "module": pipeline, - "id": pipeline.id if hasattr(pipeline, "id") else loaded_module.__name__, - "name": pipeline.name if hasattr(pipeline, "name") else loaded_module.__name__, - } +on_startup() from contextlib import asynccontextmanager @@ -60,7 +78,6 @@ async def lifespan(app: FastAPI): yield for pipeline in PIPELINES.values(): - if hasattr(pipeline["module"], "on_shutdown"): await pipeline["module"].on_shutdown() @@ -126,14 +143,15 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): ) def job(): - logging.info(form_data.model) + print(form_data.model) get_response = app.state.PIPELINES[form_data.model]["module"].get_response if form_data.stream: def stream_content(): res = get_response( - user_message, + user_message=user_message, + model_id=form_data.model, messages=messages, body=form_data.model_dump(), ) @@ -186,7 +204,8 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): return StreamingResponse(stream_content(), media_type="text/event-stream") else: res = get_response( - user_message, + user_message=user_message, + model_id=form_data.model, messages=messages, body=form_data.model_dump(), ) diff --git a/pipelines/examples/applescript_pipeline.py b/pipelines/examples/applescript_pipeline.py index 3453ddc..c61bd6c 100644 --- a/pipelines/examples/applescript_pipeline.py +++ b/pipelines/examples/applescript_pipeline.py @@ -25,7 +25,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/azure_openai_pipeline.py b/pipelines/examples/azure_openai_pipeline.py index 6b746d0..ed6b298 100644 --- a/pipelines/examples/azure_openai_pipeline.py +++ b/pipelines/examples/azure_openai_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/haystack_pipeline.py b/pipelines/examples/haystack_pipeline.py index 650a771..91c62ad 100644 --- a/pipelines/examples/haystack_pipeline.py +++ b/pipelines/examples/haystack_pipeline.py @@ -79,7 +79,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + self, user_message: str, model_id: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/llama_cpp_pipeline.py b/pipelines/examples/llama_cpp_pipeline.py index 47bf630..a35623c 100644 --- a/pipelines/examples/llama_cpp_pipeline.py +++ b/pipelines/examples/llama_cpp_pipeline.py @@ -30,7 +30,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/llamaindex_ollama_github_pipeline.py b/pipelines/examples/llamaindex_ollama_github_pipeline.py index 2f091d6..992bb0f 100644 --- a/pipelines/examples/llamaindex_ollama_github_pipeline.py +++ b/pipelines/examples/llamaindex_ollama_github_pipeline.py @@ -70,7 +70,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + self, user_message: str, model_id: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/llamaindex_ollama_pipeline.py b/pipelines/examples/llamaindex_ollama_pipeline.py index 19ed721..2ea1638 100644 --- a/pipelines/examples/llamaindex_ollama_pipeline.py +++ b/pipelines/examples/llamaindex_ollama_pipeline.py @@ -30,7 +30,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + self, user_message: str, model_id: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/llamaindex_pipeline.py b/pipelines/examples/llamaindex_pipeline.py index 195e3f8..d6fccf9 100644 --- a/pipelines/examples/llamaindex_pipeline.py +++ b/pipelines/examples/llamaindex_pipeline.py @@ -25,7 +25,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + self, user_message: str, model_id: str, messages: List[dict], body: dict ) -> Union[str, Generator, Iterator]: # This is where you can add your custom RAG pipeline. # Typically, you would retrieve relevant information from your knowledge base and synthesize it to generate a response. diff --git a/pipelines/examples/manifold_pipeline.py b/pipelines/examples/manifold_pipeline.py new file mode 100644 index 0000000..c0eda65 --- /dev/null +++ b/pipelines/examples/manifold_pipeline.py @@ -0,0 +1,43 @@ +from typing import List, Union, Generator, Iterator +from schemas import OpenAIChatMessage + + +class Pipeline: + def __init__(self): + # Optionally, you can set the id and name of the pipeline. + self.id = "pipeline_example" + self.name = "Pipeline Example" + # You can also set the pipelines that are available in this pipeline. + self.pipelines = [ + { + "id": "pipeline-1", + "name": "Pipeline 1", + }, + { + "id": "pipeline-2", + "name": "Pipeline 2", + }, + ] + 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 + + def get_response( + 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"get_response:{__name__}") + + print(messages) + print(user_message) + print(body) + + return f"{__name__} response to: {user_message}" diff --git a/pipelines/examples/mlx_pipeline.py b/pipelines/examples/mlx_pipeline.py index 8487d8e..a79dcd6 100644 --- a/pipelines/examples/mlx_pipeline.py +++ b/pipelines/examples/mlx_pipeline.py @@ -73,7 +73,7 @@ class Pipeline: print(f"Failed to terminate subprocess: {e}") def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/ollama_pipeline.py b/pipelines/examples/ollama_pipeline.py index 876380a..53da4c8 100644 --- a/pipelines/examples/ollama_pipeline.py +++ b/pipelines/examples/ollama_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/openai_pipeline.py b/pipelines/examples/openai_pipeline.py index f4273e7..98a42d5 100644 --- a/pipelines/examples/openai_pipeline.py +++ b/pipelines/examples/openai_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/pipeline_example.py b/pipelines/examples/pipeline_example.py index 1015ba8..0fba805 100644 --- a/pipelines/examples/pipeline_example.py +++ b/pipelines/examples/pipeline_example.py @@ -20,7 +20,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/examples/python_code_pipeline.py b/pipelines/examples/python_code_pipeline.py index 8d3aa20..7e35655 100644 --- a/pipelines/examples/python_code_pipeline.py +++ b/pipelines/examples/python_code_pipeline.py @@ -31,7 +31,7 @@ class Pipeline: return e.output.strip(), e.returncode def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") diff --git a/pipelines/manifold_pipeline.py b/pipelines/manifold_pipeline.py new file mode 100644 index 0000000..b5d141c --- /dev/null +++ b/pipelines/manifold_pipeline.py @@ -0,0 +1,45 @@ +from typing import List, Union, Generator, Iterator +from schemas import OpenAIChatMessage + + +class Pipeline: + def __init__(self): + self.id = "manifold_pipeline" + self.name = "Manifold Pipeline" + # 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. + # Manifold pipelines can have multiple pipelines. + self.manifold = True + self.pipelines = [ + { + "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` + "name": "Manifold: Pipeline 1", + }, + { + "id": "pipeline-2", + "name": "Manifold: Pipeline 2", + }, + ] + 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 + + def get_response( + 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"get_response:{__name__}") + + print(messages) + print(user_message) + print(body) + + return f"{model_id} response to: {user_message}" diff --git a/pipelines/ollama_pipeline.py b/pipelines/ollama_pipeline.py index 876380a..53da4c8 100644 --- a/pipelines/ollama_pipeline.py +++ b/pipelines/ollama_pipeline.py @@ -21,7 +21,7 @@ class Pipeline: pass def get_response( - self, user_message: str, messages: List[dict], body: dict + 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"get_response:{__name__}") From ad5846eb6a5dddcf94aba4bd9cd19575e7bee779 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:05:03 -0700 Subject: [PATCH 12/20] Update manifold_pipeline.py --- pipelines/examples/manifold_pipeline.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pipelines/examples/manifold_pipeline.py b/pipelines/examples/manifold_pipeline.py index c0eda65..b5d141c 100644 --- a/pipelines/examples/manifold_pipeline.py +++ b/pipelines/examples/manifold_pipeline.py @@ -4,18 +4,20 @@ from schemas import OpenAIChatMessage class Pipeline: def __init__(self): - # Optionally, you can set the id and name of the pipeline. - self.id = "pipeline_example" - self.name = "Pipeline Example" + self.id = "manifold_pipeline" + self.name = "Manifold Pipeline" # 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. + # Manifold pipelines can have multiple pipelines. + self.manifold = True self.pipelines = [ { - "id": "pipeline-1", - "name": "Pipeline 1", + "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` + "name": "Manifold: Pipeline 1", }, { "id": "pipeline-2", - "name": "Pipeline 2", + "name": "Manifold: Pipeline 2", }, ] pass @@ -40,4 +42,4 @@ class Pipeline: print(user_message) print(body) - return f"{__name__} response to: {user_message}" + return f"{model_id} response to: {user_message}" From 29e3ba4c4d23648544862edc135049c08242398d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:07:31 -0700 Subject: [PATCH 13/20] refac --- main.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 47f61b5..ba751a2 100644 --- a/main.py +++ b/main.py @@ -51,6 +51,7 @@ def on_startup(): "module": pipeline, "id": manifold_pipeline_id, "name": p["name"], + "manifold": True, } else: PIPELINES[loaded_module.__name__] = { @@ -144,14 +145,21 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): def job(): print(form_data.model) - get_response = app.state.PIPELINES[form_data.model]["module"].get_response + + pipeline = app.state.PIPELINES[form_data.model] + pipeline_id = form_data.model + + if pipeline.get("manifold", False): + pipeline_id = pipeline_id.split(".")[1] + + get_response = pipeline["module"].get_response if form_data.stream: def stream_content(): res = get_response( user_message=user_message, - model_id=form_data.model, + model_id=pipeline_id, messages=messages, body=form_data.model_dump(), ) @@ -205,7 +213,7 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): else: res = get_response( user_message=user_message, - model_id=form_data.model, + model_id=pipeline_id, messages=messages, body=form_data.model_dump(), ) From 918b709f30d5049f5921d9b87334ad75a240529a Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:12:01 -0700 Subject: [PATCH 14/20] feat: manifold naming --- main.py | 7 ++- pipelines/examples/manifold_pipeline.py | 7 +-- .../examples/ollama_manifold_pipeline.py | 45 +++++++++++++++++++ pipelines/manifold_pipeline.py | 7 +-- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 pipelines/examples/ollama_manifold_pipeline.py diff --git a/main.py b/main.py index ba751a2..d85e125 100644 --- a/main.py +++ b/main.py @@ -47,10 +47,15 @@ def on_startup(): if hasattr(pipeline, "manifold") and pipeline.manifold: for p in pipeline.pipelines: manifold_pipeline_id = f'{pipeline_id}.{p["id"]}' + + manifold_pipeline_name = p["name"] + if hasattr(pipeline, "name"): + manifold_pipeline_name = f"{pipeline.name}{manifold_pipeline_name}" + PIPELINES[manifold_pipeline_id] = { "module": pipeline, "id": manifold_pipeline_id, - "name": p["name"], + "name": manifold_pipeline_name, "manifold": True, } else: diff --git a/pipelines/examples/manifold_pipeline.py b/pipelines/examples/manifold_pipeline.py index b5d141c..c1d6934 100644 --- a/pipelines/examples/manifold_pipeline.py +++ b/pipelines/examples/manifold_pipeline.py @@ -5,7 +5,8 @@ from schemas import OpenAIChatMessage class Pipeline: def __init__(self): self.id = "manifold_pipeline" - self.name = "Manifold Pipeline" + # Optionally, you can set the name of the manifold pipeline. + self.name = "Manifold: " # 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. # Manifold pipelines can have multiple pipelines. @@ -13,11 +14,11 @@ class Pipeline: self.pipelines = [ { "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` - "name": "Manifold: Pipeline 1", + "name": "Pipeline 1", # This will turn into `Manifold: Pipeline 1` }, { "id": "pipeline-2", - "name": "Manifold: Pipeline 2", + "name": "Pipeline 2", }, ] pass diff --git a/pipelines/examples/ollama_manifold_pipeline.py b/pipelines/examples/ollama_manifold_pipeline.py new file mode 100644 index 0000000..b5d141c --- /dev/null +++ b/pipelines/examples/ollama_manifold_pipeline.py @@ -0,0 +1,45 @@ +from typing import List, Union, Generator, Iterator +from schemas import OpenAIChatMessage + + +class Pipeline: + def __init__(self): + self.id = "manifold_pipeline" + self.name = "Manifold Pipeline" + # 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. + # Manifold pipelines can have multiple pipelines. + self.manifold = True + self.pipelines = [ + { + "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` + "name": "Manifold: Pipeline 1", + }, + { + "id": "pipeline-2", + "name": "Manifold: Pipeline 2", + }, + ] + 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 + + def get_response( + 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"get_response:{__name__}") + + print(messages) + print(user_message) + print(body) + + return f"{model_id} response to: {user_message}" diff --git a/pipelines/manifold_pipeline.py b/pipelines/manifold_pipeline.py index b5d141c..c1d6934 100644 --- a/pipelines/manifold_pipeline.py +++ b/pipelines/manifold_pipeline.py @@ -5,7 +5,8 @@ from schemas import OpenAIChatMessage class Pipeline: def __init__(self): self.id = "manifold_pipeline" - self.name = "Manifold Pipeline" + # Optionally, you can set the name of the manifold pipeline. + self.name = "Manifold: " # 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. # Manifold pipelines can have multiple pipelines. @@ -13,11 +14,11 @@ class Pipeline: self.pipelines = [ { "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` - "name": "Manifold: Pipeline 1", + "name": "Pipeline 1", # This will turn into `Manifold: Pipeline 1` }, { "id": "pipeline-2", - "name": "Manifold: Pipeline 2", + "name": "Pipeline 2", }, ] pass From b2b1dd853c8bce3becd96d0b22ee74b2eaf1eaa8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:17:51 -0700 Subject: [PATCH 15/20] feat: ollama manifold pipeline example --- .../examples/ollama_manifold_pipeline.py | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/pipelines/examples/ollama_manifold_pipeline.py b/pipelines/examples/ollama_manifold_pipeline.py index b5d141c..752fb6e 100644 --- a/pipelines/examples/ollama_manifold_pipeline.py +++ b/pipelines/examples/ollama_manifold_pipeline.py @@ -1,27 +1,30 @@ from typing import List, Union, Generator, Iterator from schemas import OpenAIChatMessage +import requests class Pipeline: def __init__(self): - self.id = "manifold_pipeline" - self.name = "Manifold Pipeline" # 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. # Manifold pipelines can have multiple pipelines. self.manifold = True - self.pipelines = [ - { - "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` - "name": "Manifold: Pipeline 1", - }, - { - "id": "pipeline-2", - "name": "Manifold: Pipeline 2", - }, - ] + self.id = "ollama_manifold" + # Optionally, you can set the name of the manifold pipeline. + self.name = "Ollama: " + + self.OLLAMA_BASE_URL = "http://localhost:11434" + self.pipelines = self.get_ollama_models() pass + def get_ollama_models(self): + r = requests.get(f"{self.OLLAMA_BASE_URL}/api/tags") + models = r.json() + + return [ + {"id": model["model"], "name": model["name"]} for model in models["models"] + ] + async def on_startup(self): # This function is called when the server is started. print(f"on_startup:{__name__}") @@ -36,10 +39,25 @@ class Pipeline: 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"get_response:{__name__}") - print(messages) - print(user_message) - print(body) + if "user" in body: + print("######################################") + print(f'# User: {body["user"]["name"]} ({body["user"]["id"]})') + print(f"# Message: {user_message}") + print("######################################") - return f"{model_id} response to: {user_message}" + try: + r = requests.post( + url=f"{self.OLLAMA_BASE_URL}/v1/chat/completions", + json={**body, "model": model_id}, + 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}" From 89a234365a27e28c59984f51b6338958e16c4735 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:19:47 -0700 Subject: [PATCH 16/20] refac --- pipelines/examples/manifold_pipeline.py | 7 ++-- pipelines/manifold_pipeline.py | 46 ------------------------- 2 files changed, 4 insertions(+), 49 deletions(-) delete mode 100644 pipelines/manifold_pipeline.py diff --git a/pipelines/examples/manifold_pipeline.py b/pipelines/examples/manifold_pipeline.py index c1d6934..99e71e2 100644 --- a/pipelines/examples/manifold_pipeline.py +++ b/pipelines/examples/manifold_pipeline.py @@ -4,13 +4,14 @@ from schemas import OpenAIChatMessage class Pipeline: def __init__(self): - self.id = "manifold_pipeline" - # Optionally, you can set the name of the manifold pipeline. - self.name = "Manifold: " # 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. # Manifold pipelines can have multiple pipelines. self.manifold = True + + self.id = "manifold_pipeline" + # Optionally, you can set the name of the manifold pipeline. + self.name = "Manifold: " self.pipelines = [ { "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` diff --git a/pipelines/manifold_pipeline.py b/pipelines/manifold_pipeline.py deleted file mode 100644 index c1d6934..0000000 --- a/pipelines/manifold_pipeline.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import List, Union, Generator, Iterator -from schemas import OpenAIChatMessage - - -class Pipeline: - def __init__(self): - self.id = "manifold_pipeline" - # Optionally, you can set the name of the manifold pipeline. - self.name = "Manifold: " - # 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. - # Manifold pipelines can have multiple pipelines. - self.manifold = True - self.pipelines = [ - { - "id": "pipeline-1", # This will turn into `manifold_pipeline.pipeline-1` - "name": "Pipeline 1", # This will turn into `Manifold: Pipeline 1` - }, - { - "id": "pipeline-2", - "name": "Pipeline 2", - }, - ] - 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 - - def get_response( - 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"get_response:{__name__}") - - print(messages) - print(user_message) - print(body) - - return f"{model_id} response to: {user_message}" From 6dbe29d73f8f899eaab3b13613edcd8d6a80aceb Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 16:49:06 -0700 Subject: [PATCH 17/20] doc: readme --- README.md | 4 ++++ header.png | Bin 0 -> 29457 bytes 2 files changed, 4 insertions(+) create mode 100644 header.png diff --git a/README.md b/README.md index 031b76b..26e7282 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ Pipelines Logo +

+ # Pipelines: UI-Agnostic OpenAI API Plugin Framework Welcome to **Pipelines**, [Open WebUI](https://github.com/open-webui) initiative that brings modular, customizable workflows to any UI client supporting OpenAI API specs – and much more! Dive into a world where you can effortlessly extend functionalities, integrate unique logic, and create dynamic agentic workflows, all with a few lines of code. diff --git a/header.png b/header.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd769ca7800b20f86a7ccd0c6d1ff337955b694 GIT binary patch literal 29457 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 literal 0 HcmV?d00001 From c697e265e78e431b020d4d1d3515d3a9df760da2 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 23:17:59 -0700 Subject: [PATCH 18/20] Update header.png --- header.png | Bin 29457 -> 28794 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/header.png b/header.png index 1cd769ca7800b20f86a7ccd0c6d1ff337955b694..2f2e9b14b15932948e7f169083d21b534c179bd1 100644 GIT binary patch literal 28794 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 CIGCdV literal 29457 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 From 8c3f262ffd18467856e58cca4449f08442e59674 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Sun, 26 May 2024 23:54:26 -0700 Subject: [PATCH 19/20] refac --- main.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index d85e125..b309c9a 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,7 @@ import logging from concurrent.futures import ThreadPoolExecutor PIPELINES = {} +PIPELINE_MODULES = {} def on_startup(): @@ -41,9 +42,10 @@ def on_startup(): logging.info("Loaded:", loaded_module.__name__) pipeline = loaded_module.Pipeline() - pipeline_id = pipeline.id if hasattr(pipeline, "id") else loaded_module.__name__ + PIPELINE_MODULES[pipeline_id] = pipeline + if hasattr(pipeline, "manifold") and pipeline.manifold: for p in pipeline.pipelines: manifold_pipeline_id = f'{pipeline_id}.{p["id"]}' @@ -53,14 +55,14 @@ def on_startup(): manifold_pipeline_name = f"{pipeline.name}{manifold_pipeline_name}" PIPELINES[manifold_pipeline_id] = { - "module": pipeline, + "module": pipeline_id, "id": manifold_pipeline_id, "name": manifold_pipeline_name, "manifold": True, } else: PIPELINES[loaded_module.__name__] = { - "module": pipeline, + "module": pipeline_id, "id": pipeline_id, "name": ( pipeline.name @@ -78,14 +80,14 @@ from contextlib import asynccontextmanager @asynccontextmanager async def lifespan(app: FastAPI): - for pipeline in PIPELINES.values(): - if hasattr(pipeline["module"], "on_startup"): - await pipeline["module"].on_startup() + for module in PIPELINE_MODULES.values(): + if hasattr(module, "on_startup"): + await module.on_startup() yield - for pipeline in PIPELINES.values(): - if hasattr(pipeline["module"], "on_shutdown"): - await pipeline["module"].on_shutdown() + for module in PIPELINE_MODULES.values(): + if hasattr(module, "on_shutdown"): + await module.on_shutdown() app = FastAPI(docs_url="/docs", redoc_url=None, lifespan=lifespan) From 23ce9d396bdd21ecd8ee7e75c11cd27c73d56d1d Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Mon, 27 May 2024 00:18:48 -0700 Subject: [PATCH 20/20] fix --- main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index b309c9a..7796aa2 100644 --- a/main.py +++ b/main.py @@ -156,10 +156,13 @@ async def generate_openai_chat_completion(form_data: OpenAIChatCompletionForm): pipeline = app.state.PIPELINES[form_data.model] pipeline_id = form_data.model - if pipeline.get("manifold", False): - pipeline_id = pipeline_id.split(".")[1] + print(pipeline_id) - get_response = pipeline["module"].get_response + if pipeline.get("manifold", False): + manifold_id, pipeline_id = pipeline_id.split(".", 1) + get_response = PIPELINE_MODULES[manifold_id].get_response + else: + get_response = PIPELINE_MODULES[pipeline_id].get_response if form_data.stream: