Skip to content

Commit

Permalink
Add Bot baseline
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinbuss committed Aug 21, 2024
1 parent 6d01bba commit b83069b
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 0 deletions.
69 changes: 69 additions & 0 deletions code/backend/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import sys
import traceback
from datetime import datetime

from aiohttp import web
from aiohttp.web import Request, Response
from botbuilder.core import (
TurnContext,
)
from botbuilder.core.integration import aiohttp_error_middleware
from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationBotFrameworkAuthentication
from botbuilder.schema import Activity, ActivityTypes

from bots.assistant import AssistantBot
from core.config import settings as CONFIG


# Create adapter.
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
ADAPTER = CloudAdapter(ConfigurationBotFrameworkAuthentication(CONFIG))


# Catch-all for errors.
async def on_error(context: TurnContext, error: Exception):
# This check writes out errors to console log .vs. app insights.
# NOTE: In production environment, you should consider logging this to Azure
# application insights.
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
traceback.print_exc()

# Send a message to the user
await context.send_activity("The bot encountered an error or bug.")
await context.send_activity(
"To continue to run this bot, please fix the bot source code."
)
# Send a trace activity if we're talking to the Bot Framework Emulator
if context.activity.channel_id == "emulator":
# Create a trace activity that contains the error object
trace_activity = Activity(
label="TurnError",
name="on_turn_error Trace",
timestamp=datetime.now(datetime.UTC),
type=ActivityTypes.trace,
value=f"{error}",
value_type="https://www.botframework.com/schemas/error",
)
# Send a trace activity, which will be displayed in Bot Framework Emulator
await context.send_activity(trace_activity)


ADAPTER.on_turn_error = on_error

# Create the Bot
BOT = AssistantBot()


# Listen for incoming requests on /api/messages
async def messages(req: Request) -> Response:
return await ADAPTER.process(req, BOT)


APP = web.Application(middlewares=[aiohttp_error_middleware])
APP.router.add_post("/api/messages", messages)

if __name__ == "__main__":
try:
web.run_app(APP, host="localhost", port=CONFIG.PORT)
except Exception as error:
raise error
Empty file added code/backend/bots/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions code/backend/bots/assistant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import List
from botbuilder.core import ActivityHandler, MessageFactory, TurnContext
from botbuilder.schema import ChannelAccount
from llm.assisstant import assistant_handler


class AssistantBot(ActivityHandler):
thread_id = None

async def on_members_added_activity(
self, members_added: List[ChannelAccount], turn_context: TurnContext
):
"""Onboards new members to the assistant by creating a new thread and adding a initial welcome message.
members_added (List[ChannelAccount]): The list of channel accounts.
turn_context (TurnContext): The turn context.
RETURNS (str): The welcome message actvity is being returned.
"""
for member in members_added:
if member.id != turn_context.activity.recipient.id:
# Initialize thread in assistant
self.thread_id = assistant_handler.create_thread()
# Respond with welcome message
await turn_context.send_activity("Hello and welcome! I am your personal joke assistant. How can I help you today?")

async def on_message_activity(self, turn_context: TurnContext):
"""Acts upon new messages added to a channel.
turn_context (TurnContext): The turn context.
RETURNS (str): The assistant message actvity is being returned.
"""
# Interact with assistant
message = assistant_handler.send_message(
message=turn_context.activity.text,
thread_id=self.thread_id,
)
if message:
return await turn_context.send_activity(
MessageFactory.text(message)
)
Empty file added code/backend/core/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions code/backend/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging

from pydantic import Field
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
# General project settings
PROJECT_NAME: str = "BotAssistantSample"
SERVER_NAME: str = "BotAssistantSample"
APP_VERSION: str = "v0.0.1"
PORT: int = 3978

# Logging settings
LOGGING_LEVEL: int = logging.INFO
DEBUG: bool = False
APPLICATIONINSIGHTS_CONNECTION_STRING: str = Field(
default="", alias="APPLICATIONINSIGHTS_CONNECTION_STRING"
)

# Authentication settings
APP_ID: str = Field(default="", alias="MICROSOFT_APP_ID")
APP_PASSWORD : str = Field(default="", alias="MICROSOFT_APP_PASSWORD")
APP_TENANTID : str = Field(default="", alias="MICROSOFT_APP_TENANTID")
APP_TYPE : str = Field(default="MultiTenant", alias="MICROSOFT_APP_TYPE")

# Azure Open AI settings
AZURE_OPEN_AI_ENDPOINT: str
AZURE_OPEN_AI_API_VERSION: str
AZURE_OPENAI_API_KEY: str
AZURE_OPENAI_ASSISTANT_ID: str


settings = Settings()
Empty file added code/backend/llm/__init__.py
Empty file.
106 changes: 106 additions & 0 deletions code/backend/llm/assisstant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import time
import logging
import json

from core.config import settings

from openai import AzureOpenAI
from openai.types.beta.threads import Run

class AssistantHandler:
def __init__(self, *args,) -> None:
self.client = AzureOpenAI(
api_key=settings.AZURE_OPENAI_API_KEY,
api_version=settings.AZURE_OPEN_AI_API_VERSION,
azure_endpoint=settings.AZURE_OPEN_AI_ENDPOINT,
)
self.assistant_id = settings.AZURE_OPENAI_ASSISTANT_ID

def create_thread(self) -> str:
"""Create a thread in the assistant.
RETURNS (str): Thread id of the newly created thread.
"""
thread = self.client.beta.threads.create()
logging.debug(f"Created thread with thread id: '{thread.id}'")
return thread.id

def send_message(self, message: str, thread_id: str) -> str | None:
"""Send a message to the thread and return the response from the assistant.
message (str): The message to be sent to the assistant.
thread_id (str): The thread id to which the message should be sent to the assistant.
RETURNS (str): The response from the assistant is being returned.
"""
logging.debug(f"Adding message to thread with thread id: '{thread_id}' - Mesage: '{message}'")
if thread_id is None:
return None

message = self.client.beta.threads.messages.create(
thread_id=thread_id,
content=message,
role="user",
)

run = self.client.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=self.assistant_id,
)
run = self.__wait_for_run(run=run, thread_id=thread_id)
run = self.__check_for_tools(run=run, thread_id=thread_id)
message = self.__get_assisstant_response(thread_id=thread_id)

return message

def __wait_for_run(self, run: Run, thread_id: str) -> Run:
"""Wait for the run to complete and return the run once completed.
run (Run): The run object of the assistant.
thread_id (str): The thread id to which the message should be sent to the assistant.
RETURNS (Run): The run that completed is being returned.
"""
while run.status not in ["completed", "cancelled", "expired", "failed"]:
time.sleep(0.5)
run = self.client.beta.threads.runs.retrieve(
thread_id=thread_id,
run_id=run.id
)
status = run.status
logging.debug(f"Status of run '{run.id}' in thread '{thread_id}': {status}")
return run

def __check_for_tools(self, run: Run, thread_id: str) -> Run:
"""Acts upon tools configured for the assistant. Runs the thread afterwards and returns the completed run.
run (Run): The run object of the assistant.
thread_id (str): The thread id to which the message should be sent to the assistant.
RETURNS (Run): The run that completed is being returned.
"""
if run.required_action:
pass

# Do Action for Function and restart run after submitting action
# run = self.wait_for_run(run=run, thread_id=thread_id)
return run

def __get_assisstant_response(self, thread_id: str) -> str:
"""Gets the latest response from the assistant thread.
thread_id (str): The thread id from which the latest message should be fetched.
RETURNS (str): The latest message from the assistant in the thread.
"""
# Get message list and load as json object
messages = self.client.beta.threads.messages.list(
thread_id=thread_id,
)
messages_json = json.loads(messages.model_dump_json())

# Extract message from json object
message_data_0 = messages_json.get("data", [{"content": [{"text": {"value": ""}}]}]).pop(0)
message_data_0_content_0 = message_data_0.get("content", [{"text": {"value": ""}}]).pop(0)
first_message_text = message_data_0_content_0.get("text", {"value": ""}).get("value")
logging.debug(f"Response from Assistant in thread '{thread_id}': {first_message_text}")

return first_message_text

assistant_handler = AssistantHandler()
5 changes: 5 additions & 0 deletions code/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
botbuilder-core~=4.16.1
botbuilder-integration-aiohttp~=4.16.1
pydantic-settings~=2.2.1
pydantic~=2.7.0
openai~=1.42.0

0 comments on commit b83069b

Please sign in to comment.