diff --git a/docs/source/conf.py b/docs/source/conf.py index 343e64a46..2f930d977 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -155,9 +155,10 @@ def setup(_): ("tutorials.context_storages", "Context Storages"), ( "tutorials.messengers", - "Messengers", + "Interfaces", [ ("telegram", "Telegram"), + ("web_api_interface", "Web API"), ], ), ("tutorials.pipeline", "Pipeline"), diff --git a/setup.py b/setup.py index 5e3e2c923..162c8046b 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,14 @@ def merge_req_lists(*req_lists: List[str]) -> List[str]: "flask[async]>=2.1.2", "psutil>=5.9.1", "telethon>=1.27.0,<2.0", + "anyio>=3.6.2", + "fastapi>=0.95.1", + "idna>=3.4", + "sniffio>=1.3.0", + "starlette>=0.26.1", + "h11>=0.14.0", + "uvicorn>=0.21.1", + "websockets>=11.0", ], requests_requirements, ) diff --git a/tutorials/context_storages/8_json_storage_with_web_api.py b/tutorials/context_storages/8_json_storage_with_web_api.py deleted file mode 100644 index c110f23ad..000000000 --- a/tutorials/context_storages/8_json_storage_with_web_api.py +++ /dev/null @@ -1,46 +0,0 @@ -# %% [markdown] -""" -# 8. JSON storage with web API - -This is a tutorial on using JSON with web API. -""" - - -# %% -import pathlib - -from dff.context_storages import context_storage_factory - -from dff.pipeline import Pipeline -from dff.utils.testing.common import check_happy_path, is_interactive_mode -from dff.utils.testing.toy_script import TOY_SCRIPT_ARGS, HAPPY_PATH - -from flask import Flask, request - - -# %% -app = Flask(__name__) - -pathlib.Path("dbs").mkdir(exist_ok=True) -db = context_storage_factory("json://dbs/file.json") - - -@app.route("/chat", methods=["GET", "POST"]) -def respond(): - user_id = str(request.values.get("id")) - user_message = str(request.values.get("message")) - context = pipeline(user_message, user_id) - return {"response": str(context.last_response)} - - -# %% -pipeline = Pipeline.from_script(*TOY_SCRIPT_ARGS, context_storage=db) - - -# %% -if __name__ == "__main__": - check_happy_path(pipeline, HAPPY_PATH) - if is_interactive_mode(): - app.run( - host="0.0.0.0", port=5000, debug=True - ) # This runs tutorial in interactive mode (via flask, as a web server) diff --git a/tutorials/messengers/web_api_interface/1_fastapi.py b/tutorials/messengers/web_api_interface/1_fastapi.py new file mode 100644 index 000000000..c4256521f --- /dev/null +++ b/tutorials/messengers/web_api_interface/1_fastapi.py @@ -0,0 +1,53 @@ +# %% [markdown] +""" +# Web API: 1. FastAPI + +This tutorial shows how to create an API for DFF using FastAPI. + +You can see the result at http://127.0.0.1:8000/docs. +""" + + +# %% +from dff.script import Message +from dff.pipeline import Pipeline +from dff.utils.testing import TOY_SCRIPT, is_interactive_mode + +import uvicorn +from pydantic import BaseModel +from fastapi import FastAPI + + +# %% +pipeline = Pipeline.from_script( + TOY_SCRIPT, ("greeting_flow", "start_node"), ("greeting_flow", "fallback_node") +) + + +# %% +app = FastAPI() + + +class Output(BaseModel): + user_id: int + response: Message + + +@app.post("/chat", response_model=Output) +async def respond( + user_id: int, + user_message: str, +): + request = Message(text=user_message) + context = await pipeline._run_pipeline(request, user_id) # run in async + return {"user_id": user_id, "response": context.last_response} + + +# %% +if __name__ == "__main__": + if is_interactive_mode(): # do not run this during doc building + uvicorn.run( + app, + host="127.0.0.1", + port=8000, + ) diff --git a/tutorials/messengers/web_api_interface/2_websocket_chat.py b/tutorials/messengers/web_api_interface/2_websocket_chat.py new file mode 100644 index 000000000..dd8010e9e --- /dev/null +++ b/tutorials/messengers/web_api_interface/2_websocket_chat.py @@ -0,0 +1,105 @@ +# %% [markdown] +""" +# Web API: 2. WebSocket Chat + +This tutorial shows how to create a Web chat on FastAPI using websockets. + +You can see the result at http://127.0.0.1:8000/. + +This tutorial is a modified version of the FastAPI tutorial on WebSockets: +https://fastapi.tiangolo.com/advanced/websockets/. + +As mentioned in that tutorial, + +> ... for this example, we'll use a very simple HTML document with some JavaScript, +> all inside a long string. +> This, of course, is not optimal and you wouldn't use it for production. +""" + + +# %% +from dff.script import Message +from dff.pipeline import Pipeline +from dff.utils.testing import TOY_SCRIPT, is_interactive_mode + +import uvicorn +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse + + +# %% +pipeline = Pipeline.from_script( + TOY_SCRIPT, ("greeting_flow", "start_node"), ("greeting_flow", "fallback_node") +) + + +# %% +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + +
+ + + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await websocket.accept() + try: + while True: + data = await websocket.receive_text() + await websocket.send_text(f"User: {data}") + request = Message(text=data) + context = await pipeline._run_pipeline(request, client_id) + response = context.last_response.text + if response is not None: + await websocket.send_text(f"Bot: {response}") + else: + await websocket.send_text("Bot did not return text.") + except WebSocketDisconnect: # ignore disconnections + pass + + +# %% +if __name__ == "__main__": + if is_interactive_mode(): # do not run this during doc building + uvicorn.run( + app, + host="127.0.0.1", + port=8000, + )