Skip to content

Commit

Permalink
feat: add OpenAI ReAct agent with Langfuse integration and optimize M…
Browse files Browse the repository at this point in the history
…CTS prompts
  • Loading branch information
bearlike committed Dec 20, 2024
1 parent a577cf9 commit a5e83a8
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 41 deletions.
Binary file modified docs/scripts.xls
Binary file not shown.
Binary file added docs/videos/pipe_mcts.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/videos/pipe_mcts.mp4
Binary file not shown.
82 changes: 41 additions & 41 deletions python/openwebui/pipe_mcts.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,42 @@
"""
title: MCTS Answer Generation Pipe
author: KK
description: Monte Carlo Tree Search Pipe Addon for OpenWebUI with support for OpenAI and Ollama endpoints.
author: https://github.com/bearlike/scripts
requirements: langchain-openai, langfuse, pydantic
version: 1.0.0
"""

import logging
import asyncio
import random
import math
import asyncio
import json
import re
import os

# * Patch for user-id missing in the request
from types import SimpleNamespace
from typing import (
List,
Optional,
Callable,
Awaitable,
Union,
AsyncGenerator,
Awaitable,
Generator,
Optional,
Callable,
Iterator,
Union,
List,
)

import openai
from langchain_openai import ChatOpenAI
from langchain.callbacks.base import AsyncCallbackHandler
from langchain.schema import AIMessage, HumanMessage, BaseMessage


from langchain.schema import AIMessage, HumanMessage
from langfuse.callback import CallbackHandler
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field

from open_webui.constants import TASKS
# Ollama-specific imports
from open_webui.apps.ollama import main as ollama
from open_webui.constants import TASKS

# * Patch for user-id missing in the request
from types import SimpleNamespace

# Import Langfuse for logging/tracing (optional)

try:
from langfuse.callback import CallbackHandler
except ImportError:
CallbackHandler = None # Langfuse is optional

# =============================================================================

# Setup Logging

Expand Down Expand Up @@ -117,6 +108,8 @@ async def create_chat_completion(
api_key=self.openai_api_key,
model=model,
streaming=True,
model_kwargs={"extra_body": {"cache": {"no-cache": True}}},
cache=False,
callbacks=[handler], # Pass the handler here
)
# Call agenerate with messages
Expand All @@ -128,6 +121,8 @@ async def create_chat_completion(
api_key=self.openai_api_key,
model=model,
streaming=False,
cache=False,
model_kwargs={"extra_body": {"cache": {"no-cache": True}}},
)
response = await oai_model.agenerate([lc_messages])
# Extract the AIMessage from the response
Expand Down Expand Up @@ -493,13 +488,11 @@ async def emit_replace(self, content: str):

# =============================================================================

# Prompts
# Optimized Prompts

thoughts_prompt = """
<instruction>
Give a suggestion on how this answer can be improved.
WRITE ONLY AN IMPROVEMENT SUGGESTION AND NOTHING ELSE.
YOUR REPLY SHOULD BE A SINGLE SENTENCE.
In one sentence, provide a specific suggestion to improve the answer's accuracy, completeness, or clarity. Do not repeat previous suggestions or include any additional content.
</instruction>
<question>
Expand All @@ -513,46 +506,53 @@ async def emit_replace(self, content: str):

update_prompt = """
<instruction>
Your task is to read the question and the answer below, then analyze the given critique.
When you are done - think about how the answer can be improved based on the critique.
WRITE A REVISED ANSWER THAT ADDRESSES THE CRITIQUE. DO NOT WRITE ANYTHING ELSE.
Revise the answer below to address the critique and improve its quality. Provide only the updated answer without any extra explanation or repetition.
</instruction>
<question>
{question}
</question>
<draft>
{answer}
</draft>
<critique>
{critique}
</critique>
"""

eval_answer_prompt = """
Given the following text:
"{answer}"
<instruction>
Evaluate how well the answer responds to the question. Use the following scale and reply with a single number only:
How well does it answer this question:
"{question}"
- **1**: Completely incorrect or irrelevant.
- **5**: Partially correct but incomplete or unclear.
- **10**: Fully correct, comprehensive, and clear.
Rate the answer from 1 to 10, where 1 is completely wrong or irrelevant and 10 is a perfect answer.
Reply with a single number between 1 and 10 only. Do not write anything else, it will be discarded.
Do not include any additional text.
</instruction>
<question>
{question}
</question>
<answer>
{answer}
</answer>
"""

initial_prompt = """
<instruction>
Answer the question below. Do not pay attention to unexpected casing, punctuation, or accent marks.
Provide a clear, accurate, and complete answer to the question below. Consider different perspectives and avoid repeating common answers. Ignore any unexpected casing, punctuation, or accent marks.
</instruction>
<question>
{question}
</question>
"""

# =============================================================================

# Pipe Class


class Pipe:
class Valves(BaseModel):
Expand Down
195 changes: 195 additions & 0 deletions python/openwebui/pipe_react.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""
title: OpenAI ReAct + Langfuse
description: OpenAI ReAct agent using existing tools, with streaming and citations. Implemented with LangGraph.
requirements: langchain-openai, langgraph, langfuse
author: https://github.com/bearlike/scripts
version: 0.6.0
licence: MIT
"""

from typing import Callable, AsyncGenerator, Awaitable, Optional, Protocol
import os

from langgraph.prebuilt import create_react_agent
from langchain_core.tools import StructuredTool
from langfuse.callback import CallbackHandler
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from openai import OpenAI

BAD_NAMES = ["202", "13", "3.5", "chatgpt"]
EmitterType = Optional[Callable[[dict], Awaitable[None]]]


class SendCitationType(Protocol):
def __call__(self, url: str, title: str, content: str) -> Awaitable[None]: ...


class SendStatusType(Protocol):
def __call__(self, status_message: str, done: bool) -> Awaitable[None]: ...


def get_send_citation(__event_emitter__: EmitterType) -> SendCitationType:
async def send_citation(url: str, title: str, content: str):
if __event_emitter__ is None:
return
await __event_emitter__(
{
"type": "citation",
"data": {
"document": [content],
"metadata": [{"source": url, "html": False}],
"source": {"name": title},
},
}
)

return send_citation


def get_send_status(__event_emitter__: EmitterType) -> SendStatusType:
async def send_status(status_message: str, done: bool):
if __event_emitter__ is None:
return
await __event_emitter__(
{
"type": "status",
"data": {"description": status_message, "done": done},
}
)

return send_status


class Pipe:
class Valves(BaseModel):
OPENAI_BASE_URL: str = Field(
default="http://litellm:4000/v1",
description="Base URL for OpenAI API endpoints",
)
OPENAI_API_KEY: str = Field(
default="sk-CHANGE-ME", description="OpenAI API key"
)
LANGFUSE_SECRET_KEY: str = Field(
default="sk-lf-CHANGE-ME",
description="Langfuse secret key",
)
LANGFUSE_PUBLIC_KEY: str = Field(
default="pk-lf-CHANGE-ME",
description="Langfuse public key",
)
LANGFUSE_URL: str = Field(
default="http://langfuse-server:3000", description="Langfuse URL"
)
MODEL_PREFIX: str = Field(default="ReAct", description="Prefix before model ID")

def __init__(self):
self.type = "manifold"
self.valves = self.Valves(
**{k: os.getenv(k, v.default) for k, v in self.Valves.model_fields.items()}
)
print(f"{self.valves=}")

def pipes(self) -> list[dict[str, str]]:
try:
self.setup()
except Exception as e:
return [{"id": "error", "name": f"Error: {e}"}]
openai = OpenAI(**self.openai_kwargs) # type: ignore
models = [m.id for m in openai.models.list().data]
models = [m for m in models if "gpt" in m or "o1-" in m]
models = [m for m in models if not any(bad in m for bad in BAD_NAMES)]
return [{"id": m, "name": f"{self.valves.MODEL_PREFIX}/{m}"} for m in models]

def setup(self):
v = self.valves
if not v.OPENAI_API_KEY or not v.OPENAI_BASE_URL:
raise Exception("Error: OPENAI_API_KEY or OPENAI_BASE_URL is not set")
self.openai_kwargs = {
"base_url": v.OPENAI_BASE_URL,
"api_key": v.OPENAI_API_KEY,
}
lf = (v.LANGFUSE_SECRET_KEY, v.LANGFUSE_PUBLIC_KEY, v.LANGFUSE_URL)
if not all(lf):
self.langfuse_kwargs = None
else:
self.langfuse_kwargs = {
"secret_key": v.LANGFUSE_SECRET_KEY,
"public_key": v.LANGFUSE_PUBLIC_KEY,
"host": v.LANGFUSE_URL,
}

async def pipe(
self,
body: dict,
__user__: dict | None,
__task__: str | None,
__tools__: dict[str, dict] | None,
__event_emitter__: Callable[[dict], Awaitable[None]] | None,
) -> AsyncGenerator:
print(__task__)
print(f"{__tools__=}")
if __task__ == "function_calling":
return

self.setup()

model_id = body["model"][body["model"].rfind(".") + 1 :]
model = ChatOpenAI(model=model_id, **self.openai_kwargs) # type: ignore
if self.langfuse_kwargs:
user_kwargs = {"user_id": __user__["id"]} if __user__ else {}
callback_kwargs = self.langfuse_kwargs | user_kwargs
callbacks = [CallbackHandler(**callback_kwargs)] # type: ignore
else:
callbacks = []
config = {"callbacks": callbacks} # type: ignore

if __task__ == "title_generation":
content = model.invoke(body["messages"], config=config).content
assert isinstance(content, str)
yield content
return

if not __tools__:
async for chunk in model.astream(body["messages"], config=config):
content = chunk.content
assert isinstance(content, str)
yield content
return

send_citation = get_send_citation(__event_emitter__)
send_status = get_send_status(__event_emitter__)

tools = []
for key, value in __tools__.items():
tools.append(
StructuredTool(
func=None,
name=key,
coroutine=value["callable"],
args_schema=value["pydantic_model"],
description=value["spec"]["description"],
)
)
graph = create_react_agent(model, tools=tools)
inputs = {"messages": body["messages"]}
num_tool_calls = 0
async for event in graph.astream_events(inputs, version="v2", config=config): # type: ignore
kind = event["event"]
data = event["data"]
if kind == "on_chat_model_stream":
if "chunk" in data and (content := data["chunk"].content):
yield content
elif kind == "on_tool_start":
yield "\n"
await send_status(f"Running tool {event['name']}", False)
elif kind == "on_tool_end":
num_tool_calls += 1
await send_status(
f"Tool '{event['name']}' returned {data.get('output')}", True
)
await send_citation(
url=f"Tool call {num_tool_calls}",
title=event["name"],
content=f"Tool '{event['name']}' with inputs {data.get('input')} returned {data.get('output')}",
)

0 comments on commit a5e83a8

Please sign in to comment.