Skip to content

Commit

Permalink
add inital config
Browse files Browse the repository at this point in the history
  • Loading branch information
jpfcabral committed Apr 4, 2024
1 parent f6a7c5b commit a368de7
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 7 deletions.
63 changes: 59 additions & 4 deletions botgen/adapters/web_adapter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import dataclasses
import json
from datetime import datetime
from typing import Awaitable
from typing import Callable
from typing import Dict

import websockets
from aiohttp.web import Request
from botbuilder.core import BotAdapter
from botbuilder.core import TurnContext
Expand All @@ -12,17 +15,69 @@
from botbuilder.schema import ResourceResponse
from loguru import logger

from botgen.core import Bot
from botgen.core import BotMessage


clients = dict()


class WebAdapter(BotAdapter):
""" Connects PyBot to websocket or webhook """
"""Connects PyBot to websocket or webhook"""

name = "webadapter"

def __init__(self, on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None):
super().__init__(on_turn_error)

def init(self, bot: Bot):
def _init_adapter():
self.create_conversation(bot.handle_turn)

bot.ready(_init_adapter)

def create_socket_server(self, logic: callable):
"""Create a websocket server"""
logger.debug("Initing websocket server")
self.wss = websockets.serve(on_message, "localhost", 8765)

async def on_message(payload: str, ws) -> None:
try:
message: Dict = json.loads(payload)

# Note the websocket connection for this user
ws.user = message["user"]
clients[message["user"]] = ws

# This stuff normally lives inside Botkit.congfigureWebhookEndpoint
activity = Activity(
timestamp=datetime.now(),
channelId="websocket",
conversation={"id": message["user"]},
from_property={"id": message["user"]},
recipient={"id": "bot"},
channelData=message,
text=message.get("text", ""),
type=ActivityTypes.message
if message["type"] == "message"
else ActivityTypes.event,
)

# Set botkit's event type
if activity["type"] != ActivityTypes.message:
activity["channelData"]["botkitEventType"] = message["type"]

context = TurnContext(self, activity)
await self.run_pipeline(context, logic)
except json.JSONDecodeError as e:
alert = ["Error parsing incoming message from websocket."]
logger.warning("\n".join(alert))
logger.warning(e)
except Exception as ex:
logger.warning("An error occurred:", ex)

def activity_to_message(self, activity: Activity) -> BotMessage:
""" Caste a message to the simple format used by the websocket client """
"""Caste a message to the simple format used by the websocket client"""
message = BotMessage(
type=activity.type,
text=activity.text,
Expand All @@ -36,7 +91,7 @@ def activity_to_message(self, activity: Activity) -> BotMessage:
async def send_activities(
self, context: TurnContext, activities: list[Activity]
) -> ResourceResponse:
""" Standard BotBuilder adapter method to send a message from the bot to the messaging API """
"""Standard BotBuilder adapter method to send a message from the bot to the messaging API"""

responses = list()

Expand All @@ -63,7 +118,7 @@ async def update_activity(self, context: TurnContext, activity: Activity) -> Non
raise NotImplementedError()

async def delete_activity(self, context: TurnContext, reference: ConversationReference) -> None:
""" Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic """
"""Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic"""
raise NotImplementedError()

async def process_activity(self, request: Request, logic: callable):
Expand Down
124 changes: 123 additions & 1 deletion botgen/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from abc import ABC
from dataclasses import dataclass
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional

from aiohttp import web
Expand Down Expand Up @@ -53,6 +55,33 @@ class Middleware:
interpret: Callable


class BotPlugin:
"""
A plugin for Bot that can be loaded into the core bot object.
"""

def __init__(
self,
name: str,
middlewares: Optional[Dict[str, Any]] = None,
init: Optional[Callable] = None,
**kwargs: Any,
) -> None:
"""
Create a new BotPlugin instance.
Args:
name (str): The name of the plugin.
middlewares (dict): A dictionary of middleware functions that can be used to extend the bot's functionality.
init (Callable): A function that will be called when the plugin is loaded.
**kwargs: Additional arguments that can be used to configure the plugin.
"""
self.name = name
self.middlewares = middlewares
self.init = init
self.__annotations__ = kwargs


class Bot:
version: str
middleware = Middleware
Expand Down Expand Up @@ -99,6 +128,7 @@ def __init__(
self._boot_complete_handlers: list[Callable] = []

self.booted = False
self.add_dep("booted")

self._storage = MemoryStorage()

Expand All @@ -113,6 +143,14 @@ def __init__(
if self.webserver:
self.configure_webhook()

self.plugin_list = []
self._plugins = {}

if self.adapter:
self.use_plugin(self.adapter)

self.complete_dep("booted")

async def process_incoming_message(self, request: web.Request):
""" """
body = await self.adapter.process_activity(request, self.handle_turn)
Expand All @@ -121,7 +159,9 @@ async def process_incoming_message(self, request: web.Request):

def configure_webhook(self):
""" """
self.add_dep("webadapter")
self.webserver.add_routes([web.post(self.webhook_uri, self.process_incoming_message)])
self.complete_dep("webadapter")

async def handle_turn(self, turn_context: TurnContext):
""" """
Expand Down Expand Up @@ -192,7 +232,7 @@ async def _test_trigger(self, trigger: BotTrigger, message: BotMessage):

return False

async def ready(self, handler: Callable) -> None:
def ready(self, handler: Callable) -> None:
""" """

if self.booted:
Expand Down Expand Up @@ -242,3 +282,85 @@ async def spawn(

def start(self):
web.run_app(self.webserver)

def add_dep(self, name: str) -> None:
"""
Add a dependency to Bot's bootup process that must be marked as completed using complete_dep().
Parameters:
name (str): The name of the dependency that is being loaded.
"""
logger.debug(f"Waiting for {name}")
self._dependencies[name] = False

def complete_dep(self, name: str) -> bool:
"""
Mark a bootup dependency as loaded and ready to use.
Parameters:
name (str): The name of the dependency that has completed loading.
Returns:
bool: True if all dependencies have been marked complete, otherwise False.
"""
logger.debug(f"{name} ready")

self._dependencies[name] = True

if all(self._dependencies.values()):
# Everything is done!
self._signal_boot_complete()
return True
else:
return False

def _signal_boot_complete(self) -> None:
"""
This function gets called when all of the bootup dependencies are completely loaded.
"""
self.booted = True
for handler in self._boot_complete_handlers:
handler()

def use_plugin(self, plugin_or_function: Callable | BotPlugin) -> None:
"""
Load a plugin module and bind all included middlewares to their respective endpoints.
Parameters:
plugin_or_function (Callable or BotPlugin): A plugin module in the form of a function(bot) {...}
that returns {name, middlewares, init} or an object in the same form.
"""
if callable(plugin_or_function):
plugin = plugin_or_function(self)
else:
plugin = plugin_or_function

if plugin.name:
try:
self._register_plugin(plugin.name, plugin)
except Exception as err:
logger.warning(f"ERROR IN PLUGIN REGISTER: {err}")

def _register_plugin(self, name: str, endpoints: BotPlugin) -> None:
"""
Called from usePlugin to do the actual binding of middlewares for a plugin that is being loaded.
Parameters:
name (str): Name of the plugin.
endpoints (BotPlugin): The plugin object that contains middleware endpoint definitions.
"""

if name in self.plugin_list:
logger.debug("Plugin already enabled:", name)
return

self.plugin_list.append(name)

if endpoints.init:
try:
endpoints.init(self)
except Exception as err:
if err:
raise err

logger.debug("Plugin Enabled:", name)
Loading

0 comments on commit a368de7

Please sign in to comment.