Skip to content

Commit

Permalink
Add Retry Logic to http requests (#151)
Browse files Browse the repository at this point in the history
* Adds retry logic to backend via tenacity

* Adds python metadata directory to ignore.

* Adds sentry logging

* Move retry logic from API into Bloom client.

* Cleanup messagebox params and page component loading.

* Removes tenacity from /api/ deps.

* Fix code scanning alert no. 10: Information exposure through an exception

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Background task encapsulation for honcho SDK operations

* Wraps honcho operations in promise handling.

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
  • Loading branch information
bLopata and github-advanced-security[bot] authored Oct 18, 2024
1 parent b40d3e5 commit e8e787f
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 222 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ __pycache__/
*.pyc
*.pyo
.python-version
api/api.egg-info/*

# Visual Studio Code
.vscode/
Expand Down
2 changes: 1 addition & 1 deletion agent/agent/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def template(self) -> dict[str, str]:
system = (
{
"role": "system",
"content": f"""You are Bloom, a subversive-minded learning companion. Your job is to employ your theory of mind skills to predict the users mental state.
"content": f"""You are Bloom, a subversive-minded learning companion. Your job is to employ your theory of mind skills to predict the user's mental state.
Generate a thought that makes a prediction about the user's needs given current dialogue and also lists other pieces of data that would help improve your prediction
previous commentary: {self.history}""",
},
Expand Down
6 changes: 2 additions & 4 deletions api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
name = "api"
version = "0.6.0"
description = "The REST API Implementation of Tutor-GPT"
authors = [
{name = "Plastic Labs", email = "[email protected]"},
]
authors = [{ name = "Plastic Labs", email = "[email protected]" }]
requires-python = ">=3.11"
dependencies = [
"fastapi[standard]>=0.112.2",
Expand All @@ -16,4 +14,4 @@ dependencies = [

[tool.uv.sources]
# agent = { path = "../agent", editable = true }
agent = {workspace=true}
agent = { workspace = true }
214 changes: 108 additions & 106 deletions api/routers/chat.py
Original file line number Diff line number Diff line change
@@ -1,128 +1,130 @@
from typing import Optional
from fastapi import APIRouter, HTTPException, Body
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse

from api import schemas
from api.dependencies import app, honcho

from agent.chain import ThinkCall, RespondCall

import logging

router = APIRouter(prefix="/api", tags=["chat"])


@router.post("/stream")
async def stream(
inp: schemas.ConversationInput,
async def stream(inp: schemas.ConversationInput):
try:
user = honcho.apps.users.get_or_create(app_id=app.id, name=inp.user_id)

async def convo_turn():
thought = ""
response = ""
try:
thought_stream = ThinkCall(
user_input=inp.message,
app_id=app.id,
user_id=user.id,
session_id=str(inp.conversation_id),
honcho=honcho,
).stream()
for chunk in thought_stream:
thought += chunk
yield chunk

yield "❀"
response_stream = RespondCall(
user_input=inp.message,
thought=thought,
app_id=app.id,
user_id=user.id,
session_id=str(inp.conversation_id),
honcho=honcho,
).stream()
for chunk in response_stream:
response += chunk
yield chunk
yield "❀"
except Exception as e:
logging.error(f"Error during streaming: {str(e)}")
yield f"Error: {str(e)}"
return

await create_messages_and_metamessages(
app.id, user.id, inp.conversation_id, inp.message, thought, response
)

return StreamingResponse(convo_turn())
except Exception as e:
logging.error(f"An error occurred: {str(e)}")
if "rate limit" in str(e).lower():
return JSONResponse(
status_code=429,
content={
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please try again later.",
},
)
else:
return JSONResponse(
status_code=500,
content={
"error": "internal_server_error",
"message": "An internal server error has occurred.",
},
)


async def create_messages_and_metamessages(
app_id, user_id, conversation_id, user_message, thought, ai_response
):
"""Stream the response too the user, currently only used by the Web UI and has integration to be able to use Honcho is not anonymous"""
user = honcho.apps.users.get_or_create(app_id=app.id, name=inp.user_id)

def convo_turn():
thought_stream = ThinkCall(
user_input=inp.message,
app_id=app.id,
user_id=user.id,
session_id=str(inp.conversation_id),
honcho=honcho,
).stream()
thought = ""
for chunk in thought_stream:
thought += chunk
yield chunk

yield "❀"
response_stream = RespondCall(
user_input=inp.message,
thought=thought,
app_id=app.id,
user_id=user.id,
session_id=str(inp.conversation_id),
honcho=honcho,
).stream()
response = ""
for chunk in response_stream:
response += chunk
yield chunk
yield "❀"

honcho.apps.users.sessions.messages.create(
try:
# These operations will use the DB layer's built-in retry logic
await honcho.apps.users.sessions.messages.create(
is_user=True,
session_id=str(inp.conversation_id),
app_id=app.id,
user_id=user.id,
content=inp.message,
session_id=str(conversation_id),
app_id=app_id,
user_id=user_id,
content=user_message,
)
new_ai_message = honcho.apps.users.sessions.messages.create(
new_ai_message = await honcho.apps.users.sessions.messages.create(
is_user=False,
session_id=str(inp.conversation_id),
app_id=app.id,
user_id=user.id,
content=response,
session_id=str(conversation_id),
app_id=app_id,
user_id=user_id,
content=ai_response,
)
honcho.apps.users.sessions.metamessages.create(
app_id=app.id,
session_id=str(inp.conversation_id),
user_id=user.id,
await honcho.apps.users.sessions.metamessages.create(
app_id=app_id,
session_id=str(conversation_id),
user_id=user_id,
message_id=new_ai_message.id,
metamessage_type="thought",
content=thought,
)
return StreamingResponse(convo_turn())
except Exception as e:
logging.error(f"Error in create_messages_and_metamessages: {str(e)}")
raise # Re-raise the exception to be handled by the caller


@router.get("/thought/{message_id}")
async def get_thought(conversation_id: str, message_id: str, user_id: str):
user = honcho.apps.users.get_or_create(app_id=app.id, name=user_id)
thought = honcho.apps.users.sessions.metamessages.list(
session_id=conversation_id,
app_id=app.id,
user_id=user.id,
message_id=message_id,
metamessage_type="thought"
)
# In practice, there should only be one thought per message
return {"thought": thought.items[0].content if thought.items else None}

class ReactionBody(BaseModel):
reaction: Optional[str] = None

@router.post("/reaction/{message_id}")
async def add_or_remove_reaction(
conversation_id: str,
message_id: str,
user_id: str,
body: ReactionBody
):
reaction = body.reaction

if reaction is not None and reaction not in ["thumbs_up", "thumbs_down"]:
raise HTTPException(status_code=400, detail="Invalid reaction type")

user = honcho.apps.users.get_or_create(app_id=app.id, name=user_id)

message = honcho.apps.users.sessions.messages.get(
app_id=app.id,
session_id=conversation_id,
user_id=user.id,
message_id=message_id
)

if not message:
raise HTTPException(status_code=404, detail="Message not found")

metadata = message.metadata or {}

if reaction is None:
metadata.pop('reaction', None)
else:
metadata['reaction'] = reaction

honcho.apps.users.sessions.messages.update(
app_id=app.id,
session_id=conversation_id,
user_id=user.id,
message_id=message_id,
metadata=metadata
)

return {"status": "Reaction updated successfully"}
try:
user = honcho.apps.users.get_or_create(app_id=app.id, name=user_id)
thought = honcho.apps.users.sessions.metamessages.list(
session_id=conversation_id,
app_id=app.id,
user_id=user.id,
message_id=message_id,
metamessage_type="thought",
)
# In practice, there should only be one thought per message
return {"thought": thought.items[0].content if thought.items else None}
except Exception as e:
logging.error(f"An error occurred: {str(e)}")
return JSONResponse(
status_code=500,
content={
"error": "internal_server_error",
"message": "An internal server error has occurred.",
},
)
2 changes: 2 additions & 0 deletions www/components/messagebox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface MessageBoxProps {
onReactionAdded: (messageId: string, reaction: Reaction) => Promise<void>;
}

export type Reaction = "thumbs_up" | "thumbs_down" | null;

export default function MessageBox({
isUser,
userId,
Expand Down
2 changes: 2 additions & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react-toggle-dark-mode": "^1.1.1",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"retry": "^0.13.1",
"sharp": "^0.32.6",
"stripe": "^16.12.0",
"sweetalert2": "^11.14.2",
Expand All @@ -38,6 +39,7 @@
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/retry": "^0.12.5",
"@types/uuid": "^9.0.8",
"autoprefixer": "10.4.15",
"encoding": "^0.1.13",
Expand Down
35 changes: 23 additions & 12 deletions www/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e8e787f

Please sign in to comment.