Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ChatGPT wrapper pipeline #196

Merged
merged 32 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ffc5821
Add ChatGPT wrapper pipeline
wasnertobias Jan 2, 2025
24a71c1
Reformat using black
wasnertobias Jan 2, 2025
9722ece
Add system prompt
wasnertobias Jan 9, 2025
37430c6
Break lines in system prompt
wasnertobias Jan 9, 2025
5ce1691
Fix LLM config path in README
wasnertobias Jan 9, 2025
96933c0
Fix example application.yml
wasnertobias Jan 9, 2025
a30ab48
Correct OpenAPI auth
wasnertobias Jan 9, 2025
b5d4610
Reformatted using black
wasnertobias Jan 9, 2025
837494c
Fix Text exercise chat status callback dto
Hialus Jan 15, 2025
148a065
Use chat-gpt-variant instead of separate endpoint
wasnertobias Jan 15, 2025
162694b
Run black formatter
wasnertobias Jan 15, 2025
274f640
Make ChatGPTWrapper runnable once again
wasnertobias Jan 15, 2025
cdc0430
Rather call done twice instead of skip, as it produces empty results …
wasnertobias Jan 17, 2025
0659b24
Get rid of LangChain usage which is not needed
wasnertobias Jan 21, 2025
0d5dfc7
Reformant once more
wasnertobias Jan 21, 2025
1a3cb04
Add system prompt back
wasnertobias Jan 21, 2025
4d255f9
Fix response
wasnertobias Jan 21, 2025
92512b1
use proper callbacks in chatgpt wrapper
bassner Jan 21, 2025
3c97cae
handle emptyness
bassner Jan 21, 2025
0033410
logging
bassner Jan 21, 2025
92cfebb
aaaahhh
bassner Jan 21, 2025
a7a64d7
Enhanced error logging for empty model messages in OpenAIChatModel
bassner Jan 21, 2025
827d96e
wie lange ist das schon so
bassner Jan 22, 2025
3f241db
ok go
bassner Jan 22, 2025
389c196
disable post check
bassner Jan 23, 2025
9acfdd1
adapt prompt
bassner Jan 23, 2025
7141bb3
prompt
bassner Jan 23, 2025
df31bcc
prompt
bassner Jan 23, 2025
e05e0f1
Updated chat agent pipeline to refine and rewrite responses
bassner Jan 23, 2025
b33cd15
rename vars
bassner Jan 23, 2025
3e5d72b
reformat
bassner Jan 28, 2025
24db15f
flake
bassner Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ Pyris is an intermediary system that connects the [Artemis](https://github.com/l
Currently, Pyris powers [Iris](https://artemis.cit.tum.de/about-iris), a virtual AI tutor that assists students with their programming exercises on Artemis in a pedagogically meaningful way.

## Table of Contents
- [Features](#features)
- [Setup](#setup)
- [Prerequisites](#prerequisites)
- [Local Development Setup](#local-development-setup)
- [Docker Setup](#docker-setup)
- [Development Environment](#development-environment)
- [Production Environment](#production-environment)
- [Customizing Configuration](#customizing-configuration)
- [Troubleshooting](#troubleshooting)
- [Additional Notes](#additional-notes)
- [Pyris V2](#pyris-v2)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Setup](#setup)
- [Prerequisites](#prerequisites)
- [Local Development Setup](#local-development-setup)
- [Steps](#steps)
- [Docker Setup](#docker-setup)
- [Prerequisites](#prerequisites-1)
- [Docker Compose Files](#docker-compose-files)
- [Running the Containers](#running-the-containers)
- [**Development Environment**](#development-environment)
- [**Production Environment**](#production-environment)
- [**Option 1: With Nginx**](#option-1-with-nginx)
- [**Option 2: Without Nginx**](#option-2-without-nginx)
- [Managing the Containers](#managing-the-containers)
- [Customizing Configuration](#customizing-configuration)
- [Troubleshooting](#troubleshooting)

## Features

Expand Down Expand Up @@ -87,10 +95,10 @@ Currently, Pyris powers [Iris](https://artemis.cit.tum.de/about-iris), a virtual

- **Create an LLM Config File**

Create an `llm-config.local.yml` file in the root directory. You can use the provided `llm-config.example.yml` as a base.
Create an `llm_config.local.yml` file in the root directory. You can use the provided `llm_config.example.yml` as a base.

```bash
cp llm-config.example.yml llm-config.local.yml
cp llm_config.example.yml llm_config.local.yml
```

**Example OpenAI Configuration:**
Expand Down Expand Up @@ -176,15 +184,15 @@ Currently, Pyris powers [Iris](https://artemis.cit.tum.de/about-iris), a virtual
>- GPT-4 equivalent: 4
>- GPT-3.5 Turbo equivalent: 3.5

> **Warning:** Most existing pipelines in Pyris require a model with a `gpt_version_equivalent` of **4.5 or higher**. It is advised to define models in the `llm-config.local.yml` file with a `gpt_version_equivalent` of 4.5 or higher.
> **Warning:** Most existing pipelines in Pyris require a model with a `gpt_version_equivalent` of **4.5 or higher**. It is advised to define models in the `llm_config.local.yml` file with a `gpt_version_equivalent` of 4.5 or higher.

4. **Run the Server**

Start the Pyris server:

```bash
APPLICATION_YML_PATH=./application.local.yml \
LLM_CONFIG_PATH=./llm-config.local.yml \
LLM_CONFIG_PATH=./llm_config.local.yml \
uvicorn app.main:app --reload
```

Expand All @@ -203,7 +211,7 @@ Deploying Pyris using Docker ensures a consistent environment and simplifies the
- **Docker**: Install Docker from the [official website](https://www.docker.com/get-started).
- **Docker Compose**: Comes bundled with Docker Desktop or install separately on Linux.
- **Clone the Pyris Repository**: If not already done, clone the repository.
- **Create Configuration Files**: Create the `application.local.yml` and `llm-config.local.yml` files as described in the [Local Development Setup](#local-development-setup) section.
- **Create Configuration Files**: Create the `application.local.yml` and `llm_config.local.yml` files as described in the [Local Development Setup](#local-development-setup) section.

```bash
git clone https://github.com/ls1intum/Pyris.git Pyris
Expand Down Expand Up @@ -312,7 +320,7 @@ Deploying Pyris using Docker ensures a consistent environment and simplifies the

- `PYRIS_DOCKER_TAG`: Specifies the Pyris Docker image tag.
- `PYRIS_APPLICATION_YML_FILE`: Path to your `application.yml` file.
- `PYRIS_LLM_CONFIG_YML_FILE`: Path to your `llm-config.yml` file.
- `PYRIS_LLM_CONFIG_YML_FILE`: Path to your `llm_config.yml` file.
- `PYRIS_PORT`: Host port for Pyris application (default is `8000`).
- `WEAVIATE_PORT`: Host port for Weaviate REST API (default is `8001`).
- `WEAVIATE_GRPC_PORT`: Host port for Weaviate gRPC interface (default is `50051`).
Expand All @@ -321,7 +329,7 @@ Deploying Pyris using Docker ensures a consistent environment and simplifies the

Modify configuration files as needed:

- **Pyris Configuration**: Update `application.yml` and `llm-config.yml`.
- **Pyris Configuration**: Update `application.yml` and `llm_config.yml`.
- **Weaviate Configuration**: Adjust settings in `weaviate.yml`.
- **Nginx Configuration**: Modify Nginx settings in `nginx.yml` and related config files.

Expand Down
4 changes: 3 additions & 1 deletion app/domain/status/text_exercise_chat_status_update_dto.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Optional

from app.domain.status.status_update_dto import StatusUpdateDTO


class TextExerciseChatStatusUpdateDTO(StatusUpdateDTO):
result: str
result: Optional[str]
19 changes: 8 additions & 11 deletions app/llm/external/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from abc import ABCMeta, abstractmethod
from typing import Sequence, Union, Dict, Any, Type, Callable

from black import Optional
from langchain_core.tools import BaseTool
from openai.types.chat import ChatCompletionMessage
from pydantic import BaseModel
Expand Down Expand Up @@ -42,23 +44,18 @@ def __subclasshook__(cls, subclass) -> bool:

@abstractmethod
def chat(
self, messages: list[PyrisMessage], arguments: CompletionArguments
self,
messages: list[PyrisMessage],
arguments: CompletionArguments,
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
],
) -> ChatCompletionMessage:
"""Create a completion from the chat messages"""
raise NotImplementedError(
f"The LLM {self.__str__()} does not support chat completion"
)

@abstractmethod
def bind_tools(
self,
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
):
"""Bind tools"""
raise NotImplementedError(
f"The LLM {self.__str__()} does not support binding tools"
)


class EmbeddingModel(LanguageModel, metaclass=ABCMeta):
"""Abstract class for the llm embedding wrappers"""
Expand Down
34 changes: 6 additions & 28 deletions app/llm/external/ollama.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
from datetime import datetime
from typing import Literal, Any, Optional, Sequence, Union, Dict, Type, Callable

from langchain_core.language_models import LanguageModelInput
from langchain_core.messages import BaseMessage
from langchain_core.runnables import Runnable
from langchain_core.tools import BaseTool
from pydantic import Field, BaseModel

Expand Down Expand Up @@ -115,7 +112,12 @@ def complete(
return response["response"]

def chat(
self, messages: list[PyrisMessage], arguments: CompletionArguments
self,
messages: list[PyrisMessage],
arguments: CompletionArguments,
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
],
) -> PyrisMessage:
response = self._client.chat(
model=self.model,
Expand All @@ -136,29 +138,5 @@ def embed(self, text: str) -> list[float]:
)
return list(response)

"""Bind tools to the language model for function calling capabilities.

Note: Tool binding is currently not supported in Ollama models. This feature
is only available for OpenAI models.

Args:
tools: A sequence of tools to bind to the model.

Returns:
A runnable that can process inputs with the bound tools.

Raises:
NotImplementedError: Always raised as Ollama does not support tool binding.
"""

# TODO: Implement tool binding support for Ollama models
def bind_tools(
self,
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
) -> Runnable[LanguageModelInput, BaseMessage]:
raise NotImplementedError(
f"The LLM {self.__str__()} does not support binding tools"
)

def __str__(self):
return f"Ollama('{self.model}')"
42 changes: 29 additions & 13 deletions app/llm/external/openai_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from openai.types import CompletionUsage
from openai.types.chat import ChatCompletionMessage, ChatCompletionMessageParam
from openai.types.shared_params import ResponseFormatJSONObject
from pydantic import Field, BaseModel
from pydantic import BaseModel

from app.domain.data.text_message_content_dto import TextMessageContentDTO
from ...common.message_converters import map_role_to_str, map_str_to_role
Expand Down Expand Up @@ -200,12 +200,14 @@ def convert_to_iris_message(
class OpenAIChatModel(ChatModel):
model: str
api_key: str
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
] = Field(default_factory=list, alias="tools")

def chat(
self, messages: list[PyrisMessage], arguments: CompletionArguments
self,
messages: list[PyrisMessage],
arguments: CompletionArguments,
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
],
) -> PyrisMessage:
# noinspection PyTypeChecker
retries = 5
Expand All @@ -214,6 +216,11 @@ def chat(
client = self.get_client()
# Maximum wait time: 1 + 2 + 4 + 8 + 16 = 31 seconds

for message in messages:
if message.sender == "SYSTEM":
print("SYSTEM MESSAGE: " + message.contents[0].text_content)
break

messages = convert_to_open_ai_messages(messages)

for attempt in range(retries):
Expand All @@ -230,8 +237,9 @@ def chat(
type="json_object"
)

if self.tools:
params["tools"] = self.tools
if tools:
params["tools"] = [convert_to_openai_tool(tool) for tool in tools]
logging.info(f"Using tools: {tools}")

response = client.chat.completions.create(**params)
choice = response.choices[0]
Expand All @@ -243,6 +251,20 @@ def chat(
# We don't want to retry because the same message will likely be rejected again.
# Raise an exception to trigger the global error handler and report a fatal error to the client.
raise ContentFilterFinishReasonError()

if (
choice.message is None
or choice.message.content is None
or len(choice.message.content) == 0
):
logging.error("Model returned an empty message")
logging.error("Finish reason: " + choice.finish_reason)
if (
choice.message is not None
and choice.message.refusal is not None
):
logging.error("Refusal: " + choice.message.refusal)

return convert_to_iris_message(choice.message, usage, model)
except (
APIError,
Expand All @@ -255,12 +277,6 @@ def chat(
time.sleep(wait_time)
raise Exception(f"Failed to get response from OpenAI after {retries} retries")

def bind_tools(
self,
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
):
self.tools = [convert_to_openai_tool(tool) for tool in tools]


class DirectOpenAIChatModel(OpenAIChatModel):
type: Literal["openai_chat"]
Expand Down
11 changes: 8 additions & 3 deletions app/llm/langchain/iris_langchain_chat_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from langchain_core.outputs.chat_generation import ChatGeneration
from langchain_core.runnables import Runnable
from langchain_core.tools import BaseTool
from pydantic import BaseModel
from pydantic import BaseModel, Field

from app.common.PipelineEnum import PipelineEnum
from app.common.token_usage_dto import TokenUsageDTO
Expand All @@ -30,6 +30,9 @@ class IrisLangchainChatModel(BaseChatModel):
completion_args: CompletionArguments
tokens: TokenUsageDTO = None
logger: Logger = logging.getLogger(__name__)
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
] = Field(default_factory=list, alias="tools")

def __init__(
self,
Expand Down Expand Up @@ -65,7 +68,7 @@ def bind_tools(
if not tools:
raise ValueError("At least one tool must be provided")

self.request_handler.bind_tools(tools)
self.tools = tools
return self

def _generate(
Expand All @@ -77,7 +80,9 @@ def _generate(
) -> ChatResult:
iris_messages = [convert_langchain_message_to_iris_message(m) for m in messages]
self.completion_args.stop = stop
iris_message = self.request_handler.chat(iris_messages, self.completion_args)
iris_message = self.request_handler.chat(
iris_messages, self.completion_args, self.tools
)
base_message = convert_iris_message_to_langchain_message(iris_message)
chat_generation = ChatGeneration(message=base_message)
self.tokens = TokenUsageDTO(
Expand Down
9 changes: 7 additions & 2 deletions app/llm/request_handler/basic_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ def complete(
return llm.complete(prompt, arguments, image)

def chat(
self, messages: list[PyrisMessage], arguments: CompletionArguments
self,
messages: list[PyrisMessage],
arguments: CompletionArguments,
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
],
) -> PyrisMessage:
llm = self.llm_manager.get_llm_by_id(self.model_id)
return llm.chat(messages, arguments)
return llm.chat(messages, arguments, tools)

def embed(self, text: str) -> list[float]:
llm = self.llm_manager.get_llm_by_id(self.model_id)
Expand Down
11 changes: 8 additions & 3 deletions app/llm/request_handler/capability_request_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Sequence, Union, Dict, Any, Type, Callable
from typing import Sequence, Union, Dict, Any, Type, Callable, Optional

from langchain_core.tools import BaseTool
from pydantic import ConfigDict
Expand Down Expand Up @@ -50,10 +50,15 @@ def complete(self, prompt: str, arguments: CompletionArguments) -> str:
return llm.complete(prompt, arguments)

def chat(
self, messages: list[PyrisMessage], arguments: CompletionArguments
self,
messages: list[PyrisMessage],
arguments: CompletionArguments,
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
],
) -> PyrisMessage:
llm = self._select_model(ChatModel)
message = llm.chat(messages, arguments)
message = llm.chat(messages, arguments, tools)
message.token_usage.cost_per_input_token = llm.capabilities.input_cost.value
message.token_usage.cost_per_output_token = llm.capabilities.output_cost.value
return message
Expand Down
9 changes: 8 additions & 1 deletion app/llm/request_handler/request_handler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ def complete(
raise NotImplementedError

@abstractmethod
def chat(self, messages: list[any], arguments: CompletionArguments) -> PyrisMessage:
def chat(
self,
messages: list[any],
arguments: CompletionArguments,
tools: Optional[
Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]]
],
) -> PyrisMessage:
"""Create a completion from the chat messages"""
raise NotImplementedError

Expand Down
Loading