Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Retry Logic to http requests #151

Merged
merged 11 commits into from
Oct 18, 2024
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 @@ -44,7 +44,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 }
152 changes: 94 additions & 58 deletions api/routers/chat.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,118 @@
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import StreamingResponse, JSONResponse
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only nitpick is you can probably get rid of the background tasks import here, but looks good now


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,
):
"""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)
async def stream(inp: schemas.ConversationInput):
try:
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
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()
response = ""
for chunk in response_stream:
response += chunk
yield chunk
yield "❀"
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

honcho.apps.users.sessions.messages.create(
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)}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might just be I don't know how these work, but is there a change that one of these errors could occur after the generator is finished and the messages and metamessages are created?

Unclear on the relationship between the try catch block and the Streaming Response method . If there is an error in the middle of the stream what happens?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good questions! My latest commit employs a background process for the honcho calls to separately handle/log any potential errors while (ideally) not interfering with the StreamingResponse. I think in the case of say a network interruption or rate limit error, it would hit this exception and return an error to the client which would (read: should) handle this interruption gracefully - see more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My worry with this approach is that if there is an error while saving a message to honcho then that is not propagated to the front-end / user. Meaning it will look like their message sent and the conversation is fine, but if they reload the messages will be gone without any indication of an error occurring.

It might make sense to make separate try catch blocks or separate Exceptions for the different types of errors that the LLM vs the honcho calls are making and report them to the user differently, without using the background tasks

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):
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}
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."}
)
32 changes: 19 additions & 13 deletions www/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import dynamic from "next/dynamic";

import banner from "@/public/bloom2x1.svg";
import darkBanner from "@/public/bloom2x1dark.svg";
import MessageBox from "@/components/messagebox";
import Sidebar from "@/components/sidebar";
import MarkdownWrapper from "@/components/markdownWrapper";
import { DarkModeSwitch } from "react-toggle-dark-mode";
import { FaLightbulb, FaPaperPlane, FaBars } from "react-icons/fa";
import Swal from "sweetalert2";
Expand All @@ -22,7 +19,15 @@ import { getSubscription } from "@/utils/supabase/queries";
import { API } from "@/utils/api";
import { createClient } from "@/utils/supabase/client";

const Thoughts = dynamic(() => import("@/components/thoughts"));
const Thoughts = dynamic(() => import("@/components/thoughts"), {
ssr: false,
});
const MessageBox = dynamic(() => import("@/components/messagebox"), {
ssr: false,
});
const Sidebar = dynamic(() => import("@/components/sidebar"), {
ssr: false,
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick but wouldn't these components both be used immediately? Not sure what performance gain comes from this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, on further inspection, dynamically importing Messagebox likely does not improve anything other than a browser console error I believe I recall seeing - however the Sidepanel is docked on mobile by default, so there could be potentially some load speed improvement gained with this approach.


const URL = process.env.NEXT_PUBLIC_API_URL;

Expand Down Expand Up @@ -79,11 +84,9 @@ export default function Home() {
const sub = await getSubscription(supabase);
setIsSubscribed(!!sub);
}

})();
}, [supabase, posthog, userId]);


useEffect(() => {
const messageContainer = messageContainerRef.current;
if (!messageContainer) return;
Expand Down Expand Up @@ -204,7 +207,6 @@ export default function Home() {
isThinking = false;
continue;
}
console.log(value)
setThought((prev) => prev + value);
} else {
if (value.includes("❀")) {
Expand Down Expand Up @@ -300,8 +302,7 @@ export default function Home() {
isUser={message.isUser}
userId={userId}
URL={URL}
messageId={message.id}
text={message.text}
message={message}
loading={messagesLoading}
conversationId={conversationId}
setThought={setThought}
Expand All @@ -310,7 +311,7 @@ export default function Home() {
)) || (
<MessageBox
isUser={false}
text=""
message={{ id: "", text: "" }}
loading={true}
setThought={setThought}
setIsThoughtsOpen={setIsThoughtsOpen}
Expand All @@ -331,9 +332,14 @@ export default function Home() {
{/* TODO: validate input */}
<textarea
ref={input}
placeholder={isSubscribed ? "Type a message..." : "Subscribe to send messages"}
className={`flex-1 px-3 py-1 lg:px-5 lg:py-3 bg-gray-100 dark:bg-gray-800 text-gray-400 rounded-2xl border-2 resize-none ${canSend && isSubscribed ? "border-green-200" : "border-red-200 opacity-50"
}`}
placeholder={
isSubscribed ? "Type a message..." : "Subscribe to send messages"
}
className={`flex-1 px-3 py-1 lg:px-5 lg:py-3 bg-gray-100 dark:bg-gray-800 text-gray-400 rounded-2xl border-2 resize-none ${
canSend && isSubscribed
? "border-green-200"
: "border-red-200 opacity-50"
}`}
rows={1}
disabled={!isSubscribed}
onKeyDown={(e) => {
Expand Down
10 changes: 5 additions & 5 deletions www/components/messagebox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ interface MessageBoxProps {
isUser?: boolean;
userId?: string;
URL?: string;
messageId?: string;
conversationId?: string;
text: string;
message: { text: string; id: string };
loading?: boolean;
isThoughtsOpen?: boolean;
setIsThoughtsOpen: (isOpen: boolean) => void;
setThought: (thought: string) => void;
}

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

export default function MessageBox({
isUser,
userId,
URL,
messageId,
text,
message,
loading = false,
setIsThoughtsOpen,
conversationId,
setThought,
}: MessageBoxProps) {
const [isThoughtLoading, setIsThoughtLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const { id: messageId, text } = message;
const shouldShowButtons = messageId !== "";

const handleFetchThought = async () => {
Expand Down
2 changes: 2 additions & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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.11.0",
"sweetalert2": "^11.14.0",
Expand All @@ -36,6 +37,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
Loading
Loading