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 19 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]
18 changes: 18 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
app = FastAPI(default_response_class=ORJSONResponse)


def custom_openapi():
if not app.openapi_schema:
openapi_schema = FastAPI.openapi(app)
# Add security scheme
openapi_schema["components"]["securitySchemes"] = {
"bearerAuth": {"type": "apiKey", "in": "header", "name": "Authorization"}
}
# Apply the security globally
for path in openapi_schema["paths"].values():
for method in path.values():
method.setdefault("security", []).append({"bearerAuth": []})
app.openapi_schema = openapi_schema
return app.openapi_schema


app.openapi = custom_openapi


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
Expand Down
6 changes: 4 additions & 2 deletions app/pipeline/chat/course_chat_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,16 @@ def __init__(
requirements=RequirementList(
gpt_version_equivalent=4.5,
)
), completion_args=completion_args
),
completion_args=completion_args,
)
self.llm_small = IrisLangchainChatModel(
request_handler=CapabilityRequestHandler(
requirements=RequirementList(
gpt_version_equivalent=4.25,
)
), completion_args=completion_args
),
completion_args=completion_args,
)
self.callback = callback

Expand Down
4 changes: 3 additions & 1 deletion app/pipeline/chat/exercise_chat_agent_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,9 @@ def lecture_content_retrieval() -> str:
]
)

guide_response = (self.prompt | self.llm_small | StrOutputParser()).invoke(
guide_response = (
self.prompt | self.llm_small | StrOutputParser()
).invoke(
{
"response": out,
}
Expand Down
96 changes: 96 additions & 0 deletions app/pipeline/chat_gpt_wrapper_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import json
import logging
from typing import List, Optional

from langchain_core.prompts import (
ChatPromptTemplate,
)
from app.common.pyris_message import IrisMessageRole, PyrisMessage
from app.domain.chat.exercise_chat.exercise_chat_pipeline_execution_dto import (
ExerciseChatPipelineExecutionDTO,
)
from app.domain.data.text_message_content_dto import TextMessageContentDTO
from app.llm.langchain.iris_langchain_chat_model import IrisLangchainChatModel
from app.pipeline.prompts.chat_gpt_wrapper_prompts import chat_gpt_initial_system_prompt
from langchain_core.runnables import Runnable

from app.llm import CapabilityRequestHandler, RequirementList, CompletionArguments
from app.pipeline import Pipeline
from app.web.status.status_update import ChatGPTWrapperStatusCallback

logger = logging.getLogger(__name__)


def convert_chat_history_to_str(chat_history: List[PyrisMessage]) -> str:
"""
Converts the chat history to a string
:param chat_history: The chat history
:return: The chat history as a string
"""

def map_message_role(role: IrisMessageRole) -> str:
if role == IrisMessageRole.SYSTEM:
return "System"
elif role == IrisMessageRole.ASSISTANT:
return "AI Tutor"
elif role == IrisMessageRole.USER:
return "Student"
else:
return "Unknown"

return "\n\n".join(
[
f"{map_message_role(message.sender)} {"" if not message.sent_at else f"at {message.sent_at.strftime(
"%Y-%m-%d %H:%M:%S")}"}: {message.contents[0].text_content}"
for message in chat_history
]
)


class ChatGPTWrapperPipeline(Pipeline):
callback: ChatGPTWrapperStatusCallback
llm: IrisLangchainChatModel
pipeline: Runnable

def __init__(self, callback: Optional[ChatGPTWrapperStatusCallback] = None):
super().__init__(implementation_id="chat_gpt_wrapper_pipeline_reference_impl")
self.callback = callback
self.request_handler = CapabilityRequestHandler(
requirements=RequirementList(
gpt_version_equivalent=4.5,
context_length=16385,
)
)

def __call__(
self,
dto: ExerciseChatPipelineExecutionDTO,
prompt: Optional[ChatPromptTemplate] = None,
**kwargs,
):
"""
Run the ChatGPT wrapper pipeline.
This consists of a single response generation step.
"""

self.callback.in_progress()
pyris_system_prompt = PyrisMessage(
sender=IrisMessageRole.SYSTEM,
contents=[
TextMessageContentDTO(text_content=chat_gpt_initial_system_prompt)
],
)

prompts = [pyris_system_prompt] + [msg for msg in dto.chat_history if msg.contents is not None and len(msg.contents) > 0 and msg.contents[0].text_content and len(msg.contents[0].text_content) > 0]

response = self.request_handler.chat(
prompts, CompletionArguments(temperature=0.5, max_tokens=2000)
)

logger.info(f"ChatGPTWrapperPipeline response: {response}")

if response.contents is None or len(response.contents) == 0 or response.contents[0].text_content is None or len(response.contents[0].text_content) == 0:
self.callback.error("ChatGPT did not reply. Try resending.")
return

self.callback.done(final_result=response.contents[0].text_content)
4 changes: 4 additions & 0 deletions app/pipeline/prompts/chat_gpt_wrapper_prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
chat_gpt_initial_system_prompt = """
You are a helpful, smart, kind, and efficient AI assistant.
You always fulfill the user's requests to the best of your ability.
"""
3 changes: 2 additions & 1 deletion app/pipeline/shared/citation_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def create_formatted_string(self, paragraphs):
paragraph.get(LectureSchema.LECTURE_NAME.value),
paragraph.get(LectureSchema.LECTURE_UNIT_NAME.value),
paragraph.get(LectureSchema.PAGE_NUMBER.value),
paragraph.get(LectureSchema.LECTURE_UNIT_LINK.value) or "No link available",
paragraph.get(LectureSchema.LECTURE_UNIT_LINK.value)
or "No link available",
paragraph.get(LectureSchema.PAGE_TEXT_CONTENT.value),
)
formatted_string += lct
Expand Down
43 changes: 40 additions & 3 deletions app/web/routers/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from app.pipeline.chat.lecture_chat_pipeline import LectureChatPipeline
from app.web.status.status_update import (
ExerciseChatStatusCallback,
ChatGPTWrapperStatusCallback,
CourseChatStatusCallback,
CompetencyExtractionCallback,
LectureChatCallback,
Expand All @@ -31,6 +32,7 @@
)
from app.pipeline.text_exercise_chat_pipeline import TextExerciseChatPipeline
from app.web.status.status_update import TextExerciseChatCallback
from app.pipeline.chat_gpt_wrapper_pipeline import ChatGPTWrapperPipeline

router = APIRouter(prefix="/api/v1/pipelines", tags=["pipelines"])
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -74,9 +76,12 @@ def run_exercise_chat_pipeline(
description="Exercise Chat Pipeline Execution DTO"
),
):
thread = Thread(
target=run_exercise_chat_pipeline_worker, args=(dto, variant, event)
)
if variant == "chat-gpt-wrapper":
thread = Thread(target=run_chatgpt_wrapper_pipeline_worker, args=(dto, variant))
else:
thread = Thread(
target=run_exercise_chat_pipeline_worker, args=(dto, variant, event)
)
thread.start()


Expand Down Expand Up @@ -232,6 +237,30 @@ def run_competency_extraction_pipeline(
thread.start()


def run_chatgpt_wrapper_pipeline_worker(
dto: ExerciseChatPipelineExecutionDTO, _variant: str
):
try:
callback = ChatGPTWrapperStatusCallback(
run_id=dto.settings.authentication_token,
base_url=dto.settings.artemis_base_url,
initial_stages=dto.initial_stages,
)
pipeline = ChatGPTWrapperPipeline(callback=callback)
except Exception as e:
logger.error(f"Error preparing ChatGPT wrapper pipeline: {e}")
logger.error(traceback.format_exc())
capture_exception(e)
return

try:
pipeline(dto=dto)
except Exception as e:
logger.error(f"Error running ChatGPT wrapper pipeline: {e}")
logger.error(traceback.format_exc())
callback.error("Fatal error.", exception=e)


@router.get("/{feature}/variants")
def get_pipeline(feature: str):
"""
Expand Down Expand Up @@ -294,5 +323,13 @@ def get_pipeline(feature: str):
description="Default lecture chat variant.",
)
]
case "CHAT_GPT_WRAPPER":
return [
FeatureDTO(
id="default",
name="Default Variant",
description="Default ChatGPT wrapper variant.",
)
]
case _:
return Response(status_code=status.HTTP_400_BAD_REQUEST)
19 changes: 19 additions & 0 deletions app/web/status/status_update.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, List


from sentry_sdk import capture_exception, capture_message

import requests
Expand Down Expand Up @@ -222,6 +223,24 @@ def __init__(
stage = stages[current_stage_index]
super().__init__(url, run_id, status, stage, current_stage_index)

class ChatGPTWrapperStatusCallback(StatusCallback):
def __init__(
self, run_id: str, base_url: str, initial_stages: List[StageDTO] = None
):
url = f"{base_url}/api/public/pyris/pipelines/tutor-chat/runs/{run_id}/status"
current_stage_index = len(initial_stages) if initial_stages else 0
stages = initial_stages or []
stages += [
StageDTO(
weight=30,
state=StageStateEnum.NOT_STARTED,
name="Generating response",
),
]
status = ExerciseChatStatusUpdateDTO(stages=stages)
stage = stages[current_stage_index]
super().__init__(url, run_id, status, stage, current_stage_index)


class TextExerciseChatCallback(StatusCallback):
def __init__(
Expand Down
5 changes: 4 additions & 1 deletion application.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ api_keys:
weaviate:
host: "localhost"
port: "8001"
grpc_port: "50051"
grpc_port: "50051"

env_vars:
SOME: 'value'
9 changes: 0 additions & 9 deletions example_application.yml

This file was deleted.

Loading