Skip to content

Commit

Permalink
variable pricing: Add support for variable priced server bots
Browse files Browse the repository at this point in the history
- Added authorize and capture functions
- Added custom_rate_card to the bot settings
- Updated version to 0.0.50
  • Loading branch information
fhennerkes committed Dec 10, 2024
1 parent d6b0de8 commit 814f169
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 5 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "fastapi_poe"
version = "0.0.49"
version = "0.0.50"
authors = [
{ name="Lida Li", email="[email protected]" },
{ name="Jelle Zijlstra", email="[email protected]" },
Expand Down
6 changes: 5 additions & 1 deletion src/fastapi_poe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
"ToolResultDefinition",
"MessageFeedback",
"sync_bot_settings",
"CostItem",
"InsufficientFundError",
"CostRequestError",
]

from .base import PoeBot, make_app, run
from .base import CostRequestError, InsufficientFundError, PoeBot, make_app, run
from .client import (
BotError,
BotErrorNoRetry,
Expand All @@ -37,6 +40,7 @@
)
from .types import (
Attachment,
CostItem,
ErrorResponse,
MessageFeedback,
MetaResponse,
Expand Down
147 changes: 145 additions & 2 deletions src/fastapi_poe/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
)

import httpx
import httpx_sse
from fastapi import Depends, FastAPI, HTTPException, Request, Response
from fastapi.exceptions import RequestValidationError
from fastapi.responses import HTMLResponse, JSONResponse
Expand All @@ -38,6 +39,7 @@
from fastapi_poe.types import (
AttachmentUploadResponse,
ContentType,
CostItem,
ErrorResponse,
Identifier,
MetaResponse,
Expand All @@ -63,6 +65,14 @@ class AttachmentUploadError(Exception):
pass


class CostRequestError(Exception):
pass


class InsufficientFundError(Exception):
pass


class LoggingMiddleware(BaseHTTPMiddleware):
async def set_body(self, request: Request) -> None:
receive_ = await request._receive()
Expand Down Expand Up @@ -182,8 +192,13 @@ async def get_response_with_context(
response to the Poe servers. This is what gets displayed to the user.
"""
async for event in self.get_response(request):
yield event
try:
async for event in self.get_response(request):
yield event
except InsufficientFundError:
yield ErrorResponse(
allow_retry=True, error_type="insufficient_fund", text=""
)

async def get_settings(self, setting: SettingsRequest) -> SettingsResponse:
"""
Expand Down Expand Up @@ -629,6 +644,134 @@ def make_prompt_author_role_alternated(

return new_messages

async def capture_cost(
self,
request: QueryRequest,
amounts: Union[list[CostItem], CostItem],

Check failure on line 650 in src/fastapi_poe/base.py

View workflow job for this annotation

GitHub Actions / pyright

Subscript for class "list" will generate runtime exception; enclose type expression in quotes (reportIndexIssue)
access_key: Optional[str] = None,
base_url: str = "https://api.poe.com/",
) -> None:
"""
Used to capture a cost for monetized creators.
#### Parameters:
- `request` (`QueryRequest`): The currently handlded QueryRequest object.
- `amounts` (`Union[list[CostItem], CostItem]`): The to be captured amounts.
- `access_key` (`str`): The access_key corresponding to your bot. This is needed to ensure
that capture requests are coming from an authorized source.
"""

if self.access_key:
if access_key:
warnings.warn(
"Bot already has an access key, access_key parameter is not needed.",
DeprecationWarning,
stacklevel=2,
)
request_access_key = access_key
else:
request_access_key = self.access_key
else:
if access_key is None:
raise InvalidParameterError(
"access_key parameter is required if bot is not"
+ " provided with an access_key when make_app is called."
)

if not request.bot_query_id:
raise InvalidParameterError(
"bot_query_id is required to make cost requests."
)

url = f"{base_url}bot/cost/{request.bot_query_id}/capture"
result = await self._cost_requests_inner(
amounts=amounts, access_key=request_access_key, url=url

Check failure on line 690 in src/fastapi_poe/base.py

View workflow job for this annotation

GitHub Actions / pyright

"request_access_key" is possibly unbound (reportPossiblyUnboundVariable)
)
if not result:
raise InsufficientFundError()

async def authorize_cost(
self,
request: QueryRequest,
amounts: Union[list[CostItem], CostItem],

Check failure on line 698 in src/fastapi_poe/base.py

View workflow job for this annotation

GitHub Actions / pyright

Subscript for class "list" will generate runtime exception; enclose type expression in quotes (reportIndexIssue)
access_key: Optional[str] = None,
base_url: str = "https://api.poe.com/",
) -> None:
"""
Used to authorize a cost for monetized creators.
#### Parameters:
- `request` (`QueryRequest`): The currently handlded QueryRequest object.
- `amounts` (`Union[list[CostItem], CostItem]`): The to be authorized amounts.
- `access_key` (`str`): The access_key corresponding to your bot. This is needed to ensure
that authorize requests are coming from an authorized source.
"""

if self.access_key:
if access_key:
warnings.warn(
"Bot already has an access key, access_key parameter is not needed.",
DeprecationWarning,
stacklevel=2,
)
request_access_key = access_key
else:
request_access_key = self.access_key
else:
if access_key is None:
raise InvalidParameterError(
"access_key parameter is required if bot is not"
+ " provided with an access_key when make_app is called."
)

if not request.bot_query_id:
raise InvalidParameterError(
"bot_query_id is required to make cost requests."
)

url = f"{base_url}bot/cost/{request.bot_query_id}/authorize"
result = await self._cost_requests_inner(
amounts=amounts, access_key=request_access_key, url=url

Check failure on line 738 in src/fastapi_poe/base.py

View workflow job for this annotation

GitHub Actions / pyright

"request_access_key" is possibly unbound (reportPossiblyUnboundVariable)
)
if not result:
raise InsufficientFundError()

async def _cost_requests_inner(
self, amounts: Union[list[CostItem], CostItem], access_key: str, url: str

Check failure on line 744 in src/fastapi_poe/base.py

View workflow job for this annotation

GitHub Actions / pyright

Subscript for class "list" will generate runtime exception; enclose type expression in quotes (reportIndexIssue)
) -> bool:
amounts = [amounts] if isinstance(amounts, CostItem) else amounts
amounts = [amount.model_dump() for amount in amounts]

Check failure on line 747 in src/fastapi_poe/base.py

View workflow job for this annotation

GitHub Actions / pyright

Type "list[dict[str, Any]]" is not assignable to declared type "list[CostItem] | CostItem" (reportAssignmentType)
data = {"amounts": amounts, "access_key": access_key}
try:
async with httpx.AsyncClient(timeout=300) as client, httpx_sse.aconnect_sse(
client, method="POST", url=url, json=data
) as event_source:
if event_source.response.status_code != 200:
error_pieces = [
json.loads(event.data).get("message", "")
async for event in event_source.aiter_sse()
]
raise CostRequestError(
f"{event_source.response.status_code} "
f"{event_source.response.reason_phrase}: {''.join(error_pieces)}"
)

async for event in event_source.aiter_sse():
if event.event == "result":
event_data = json.loads(event.data)
result = event_data["status"]
return result == "success"
return False
except httpx.HTTPError:
logger.error(
"An HTTP error occurred when attempting to send a cost request."
)
raise

@staticmethod
def text_event(text: str) -> ServerSentEvent:
return ServerSentEvent(data=json.dumps({"text": text}), event="text")
Expand Down
17 changes: 16 additions & 1 deletion src/fastapi_poe/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Identifier: TypeAlias = str
FeedbackType: TypeAlias = Literal["like", "dislike"]
ContentType: TypeAlias = Literal["text/markdown", "text/plain"]
ErrorType: TypeAlias = Literal["user_message_too_long"]
ErrorType: TypeAlias = Literal["user_message_too_long", "insufficient_fund"]


class MessageFeedback(BaseModel):
Expand All @@ -24,6 +24,20 @@ class MessageFeedback(BaseModel):
reason: Optional[str]


class CostItem(BaseModel):
"""
An object representing a cost item used for authorization and charge request.
#### Fields:
- `amount_usd_milli_cents` (`int`)
- `description` (`str`)
"""

amount_usd_milli_cents: int
description: Optional[str] = None


class Attachment(BaseModel):
"""
Expand Down Expand Up @@ -215,6 +229,7 @@ class SettingsResponse(BaseModel):
enable_image_comprehension: Optional[bool] = None
enforce_author_role_alternation: Optional[bool] = None
enable_multi_bot_chat_prompting: Optional[bool] = None
custom_rate_card: Optional[str] = None


class AttachmentUploadResponse(BaseModel):
Expand Down

0 comments on commit 814f169

Please sign in to comment.