Skip to content

Commit

Permalink
naming changes (api bots -> server bots, api_key -> access key) (#9)
Browse files Browse the repository at this point in the history
Co-authored-by: Anmol Singh <asingh@quora.com>
  • Loading branch information
anmolsingh95 and Anmol Singh authored Aug 29, 2023
1 parent 8844fae commit 06d1140
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 56 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ if __name__ == "__main__":
## Enable authentication

Poe servers send requests containing Authorization HTTP header in the format "Bearer
<api_key>," where api_key is the API key configured in the bot settings. \
<access_key>"; the access key is configured in the bot settings page.

To validate the requests are from Poe Servers, you can either set the environment
variable POE_API_KEY or pass the parameter api_key in the run function like:
To validate that the request is from the Poe servers, you can either set the environment
variable POE_ACCESS_KEY or pass the parameter access_key in the run function like:

```python
if __name__ == "__main__":
run(EchoBot(), api_key=<key>)
run(EchoBot(), access_key=<key>)
```
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ build-backend = "hatchling.build"

[project]
name = "fastapi_poe"
version = "0.0.16"
version = "0.0.17"
authors = [
{ name="Lida Li", email="lli@quora.com" },
{ name="Jelle Zijlstra", email="jelle@quora.com" },
{ name="Anmol Singh", email="anmol@quora.com" },
]
description = "A demonstration of the Poe protocol using FastAPI"
readme = "README.md"
Expand Down
5 changes: 0 additions & 5 deletions src/fastapi_poe/__main__.py

This file was deleted.

119 changes: 86 additions & 33 deletions src/fastapi_poe/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import sys
import warnings
from typing import Any, AsyncIterable, Dict, Optional, Union

from fastapi import Depends, FastAPI, HTTPException, Request, Response
Expand Down Expand Up @@ -73,7 +74,7 @@ def auth_user(
if authorization.scheme != "Bearer" or authorization.credentials != auth_key:
raise HTTPException(
status_code=401,
detail="Invalid API key",
detail="Invalid access key",
headers={"WWW-Authenticate": "Bearer"},
)

Expand Down Expand Up @@ -174,37 +175,79 @@ async def handle_query(self, query: QueryRequest) -> AsyncIterable[ServerSentEve
yield self.done_event()


def find_auth_key(api_key: str, *, allow_without_key: bool = False) -> Optional[str]:
if not api_key:
if os.environ.get("POE_API_KEY"):
api_key = os.environ["POE_API_KEY"]
else:
if allow_without_key:
return None
print(
"Please provide an API key. You can get a key from the create_bot form at:"
)
print("https://poe.com/create_bot?api=1")
print(
"You can pass the API key to the run() function or "
"use the POE_API_KEY environment variable."
)
sys.exit(1)
if len(api_key) != 32:
print("Invalid API key (should be 32 characters)")
def _find_access_key(*, access_key: str, api_key: str) -> Optional[str]:
"""Figures out the access key.
The order of preference is:
1) access_key=
2) $POE_ACCESS_KEY
3) api_key=
4) $POE_API_KEY
"""
if access_key:
return access_key

environ_poe_access_key = os.environ.get("POE_ACCESS_KEY")
if environ_poe_access_key:
return environ_poe_access_key

if api_key:
warnings.warn(
"usage of api_key is deprecated, pass your key using access_key instead",
DeprecationWarning,
stacklevel=3,
)
return api_key

environ_poe_api_key = os.environ.get("POE_API_KEY")
if environ_poe_api_key:
warnings.warn(
"usage of POE_API_KEY is deprecated, pass your key using POE_ACCESS_KEY instead",
DeprecationWarning,
stacklevel=3,
)
return environ_poe_api_key

return None


def _verify_access_key(
*, access_key: str, api_key: str, allow_without_key: bool = False
) -> Optional[str]:
"""Checks whether we have a valid access key and returns it."""
_access_key = _find_access_key(access_key=access_key, api_key=api_key)
if not _access_key:
if allow_without_key:
return None
print(
"Please provide an access key.\n"
"You can get a key from the create_bot page at: https://poe.com/create_bot?server=1\n"
"You can then pass the key using the access_key param to the run() or make_app() "
"functions, or by using the POE_ACCESS_KEY environment variable."
)
sys.exit(1)
if len(_access_key) != 32:
print("Invalid access key (should be 32 characters)")
sys.exit(1)
return api_key
return _access_key


def make_app(
bot: PoeBot, api_key: str = "", *, allow_without_key: bool = False
bot: PoeBot,
access_key: str = "",
*,
api_key: str = "",
allow_without_key: bool = False,
) -> FastAPI:
"""Create an app object. Arguments are as for run()."""
app = FastAPI()
app.add_exception_handler(RequestValidationError, exception_handler)

global auth_key
auth_key = find_auth_key(api_key, allow_without_key=allow_without_key)
auth_key = _verify_access_key(
access_key=access_key, api_key=api_key, allow_without_key=allow_without_key
)

@app.get("/")
async def index() -> Response:
Expand All @@ -221,7 +264,11 @@ async def poe_post(request: Dict[str, Any], dict=Depends(auth_user)) -> Response
return EventSourceResponse(
bot.handle_query(
QueryRequest.parse_obj(
{**request, "api_key": auth_key or "<missing>"}
{
**request,
"access_key": auth_key or "<missing>",
"api_key": auth_key or "<missing>",
}
)
)
)
Expand All @@ -241,21 +288,31 @@ async def poe_post(request: Dict[str, Any], dict=Depends(auth_user)) -> Response
return app


def run(bot: PoeBot, api_key: str = "", *, allow_without_key: bool = False) -> None:
def run(
bot: PoeBot,
access_key: str = "",
*,
api_key: str = "",
allow_without_key: bool = False,
) -> None:
"""
Run a Poe bot server using FastAPI.
:param bot: The bot object.
:param api_key: The Poe API key to use. If not provided, it will try to read
the POE_API_KEY environment variable. If that is not set, the server will
:param access_key: The access key to use. If not provided, the server tries to read
the POE_ACCESS_KEY environment variable. If that is not set, the server will
refuse to start, unless *allow_without_key* is True.
:param allow_without_key: If True, the server will start even if no API key
is provided. Requests will not be checked against any key. If an API key
:param api_key: The previous name of access_key. This param is deprecated and will be
removed in a future version
:param allow_without_key: If True, the server will start even if no access key
is provided. Requests will not be checked against any key. If an access key
is provided, it is still checked.
"""

app = make_app(bot, api_key, allow_without_key=allow_without_key)
app = make_app(
bot, access_key=access_key, api_key=api_key, allow_without_key=allow_without_key
)

parser = argparse.ArgumentParser("FastAPI sample Poe bot server")
parser.add_argument("-p", "--port", type=int, default=8080)
Expand All @@ -270,7 +327,3 @@ def run(bot: PoeBot, api_key: str = "", *, allow_without_key: bool = False) -> N
"fmt"
] = "%(asctime)s - %(levelname)s - %(message)s"
uvicorn.run(app, host="0.0.0.0", port=port, log_config=log_config)


if __name__ == "__main__":
run(PoeBot())
47 changes: 35 additions & 12 deletions src/fastapi_poe/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""
Client for talking to other Poe bots through Poe's bot API.
Client for talking to other Poe bots through the Poe API.
"""
import asyncio
import contextlib
import json
import warnings
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, cast

Expand Down Expand Up @@ -52,7 +53,7 @@ class BotMessage:

@dataclass
class MetaMessage(BotMessage):
"""Communicate 'meta' events from API bots."""
"""Communicate 'meta' events from server bots."""

linkify: bool = True
suggested_replies: bool = True
Expand All @@ -70,13 +71,16 @@ def _safe_ellipsis(obj: object, limit: int) -> str:
@dataclass
class _BotContext:
endpoint: str
api_key: str = field(repr=False)
access_key: str = field(repr=False)
session: httpx.AsyncClient = field(repr=False)
on_error: Optional[ErrorHandler] = field(default=None, repr=False)

@property
def headers(self) -> Dict[str, str]:
return {"Accept": "application/json", "Authorization": f"Bearer {self.api_key}"}
return {
"Accept": "application/json",
"Authorization": f"Bearer {self.access_key}",
}

async def report_error(
self, message: str, metadata: Optional[Dict[str, Any]] = None
Expand Down Expand Up @@ -121,7 +125,7 @@ async def report_feedback(
)

async def fetch_settings(self) -> SettingsResponse:
"""Fetches settings from a Poe API Bot Endpoint."""
"""Fetches settings from a Poe server bot endpoint."""
resp = await self.session.post(
self.endpoint,
headers=self.headers,
Expand Down Expand Up @@ -293,27 +297,38 @@ async def _load_json_dict(


def _default_error_handler(e: Exception, msg: str) -> None:
print("Error in Poe API Bot:", msg, e)
print("Error in Poe bot:", msg, e)


async def stream_request(
request: QueryRequest,
bot_name: str,
api_key: str,
access_key: str = "",
*,
api_key: str = "",
api_key_deprecation_warning_stacklevel: int = 2,
session: Optional[httpx.AsyncClient] = None,
on_error: ErrorHandler = _default_error_handler,
num_tries: int = 2,
retry_sleep_time: float = 0.5,
base_url: str = "https://api.poe.com/bot/",
) -> AsyncGenerator[BotMessage, None]:
"""Streams BotMessages from an API bot."""
"""Streams BotMessages from a Poe bot."""
if api_key != "":
warnings.warn(
"the api_key param is deprecated, pass your key using access_key instead",
DeprecationWarning,
stacklevel=api_key_deprecation_warning_stacklevel,
)
if access_key == "":
access_key = api_key

async with contextlib.AsyncExitStack() as stack:
if session is None:
session = await stack.enter_async_context(httpx.AsyncClient())
url = f"{base_url}{bot_name}"
ctx = _BotContext(
endpoint=url, api_key=api_key, session=session, on_error=on_error
endpoint=url, access_key=access_key, session=session, on_error=on_error
)
got_response = False
for i in range(num_tries):
Expand All @@ -331,10 +346,18 @@ async def stream_request(
await asyncio.sleep(retry_sleep_time)


async def get_final_response(request: QueryRequest, bot_name: str, api_key: str) -> str:
"""Gets the final response from an API bot."""
async def get_final_response(
request: QueryRequest, bot_name: str, access_key: str = "", *, api_key: str = ""
) -> str:
"""Gets the final response from a Poe bot."""
chunks: List[str] = []
async for message in stream_request(request, bot_name, api_key):
async for message in stream_request(
request,
bot_name,
access_key,
api_key=api_key,
api_key_deprecation_warning_stacklevel=3,
):
if isinstance(message, MetaMessage):
continue
if message.is_suggested_reply:
Expand Down
4 changes: 3 additions & 1 deletion src/fastapi_poe/samples/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Sample bot that echoes back messages.
See more samples in the tutorial repo at: https://github.com/poe-platform/server-bot-tutorial
"""
from __future__ import annotations

Expand All @@ -20,4 +22,4 @@ async def get_response(self, query: QueryRequest) -> AsyncIterable[ServerSentEve


if __name__ == "__main__":
run(EchoBot())
run(EchoBot(), allow_without_key=True)
1 change: 1 addition & 0 deletions src/fastapi_poe/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class QueryRequest(BaseRequest):
message_id: Identifier
metadata: Identifier = ""
api_key: str = "<missing>"
access_key: str = "<missing>"
temperature: float = 0.7
skip_system_prompt: bool = False
logit_bias: Dict[str, float] = {}
Expand Down

0 comments on commit 06d1140

Please sign in to comment.