Skip to content

Commit b67e318

Browse files
jacoblee93nfcampos
authored andcommitted
Start of frontend changes
Time travel, use checkpoint as primary source of truth Refactor state management for chat window Add support for state graph Fixes Pare down unneeded functionality, frontend updates Fix repeated history fetches Add basic state graph support, many other fixes Revise state graph time travel flow Use message graph as default Fix flashing messages in UI on send Allow adding and deleting tool calls Hacks! Only accept module paths More logs add env add built ui files Build ui files Update cli Delete .github/workflows/build_deploy_image.yml Update path Update ui files Move migrations Move ui files 0.0.5 Allow resume execution for tool messages (#2) Undo Undo Remove cli Undo Undo Update storage/threads Undo ui Undo Lint Undo Rm Undo Rm Update api Undo WIP
1 parent d9699a6 commit b67e318

17 files changed

+1164
-153
lines changed

backend/app/api/runs.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional, Sequence
1+
from typing import Dict, Optional, Sequence, Union
22

33
import langsmith.client
44
from fastapi import APIRouter, BackgroundTasks, HTTPException
@@ -24,8 +24,8 @@ class CreateRunPayload(BaseModel):
2424
"""Payload for creating a run."""
2525

2626
thread_id: str
27-
input: Optional[Sequence[AnyMessage]] = Field(default_factory=list)
2827
config: Optional[RunnableConfig] = None
28+
input: Optional[Union[Sequence[AnyMessage], Dict]] = Field(default_factory=list)
2929

3030

3131
async def _run_input_and_config(

backend/app/api/threads.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Annotated, List, Sequence
1+
from typing import Annotated, Any, Dict, List, Optional, Sequence, Union
22
from uuid import uuid4
33

44
from fastapi import APIRouter, HTTPException, Path
@@ -21,10 +21,11 @@ class ThreadPutRequest(BaseModel):
2121
assistant_id: str = Field(..., description="The ID of the assistant to use.")
2222

2323

24-
class ThreadMessagesPostRequest(BaseModel):
24+
class ThreadPostRequest(BaseModel):
2525
"""Payload for adding messages to a thread."""
2626

27-
messages: Sequence[AnyMessage]
27+
values: Optional[Union[Dict[str, Any], Sequence[AnyMessage]]]
28+
config: Optional[Dict[str, Any]] = None
2829

2930

3031
@router.get("/")
@@ -33,23 +34,25 @@ async def list_threads(opengpts_user_id: OpengptsUserId) -> List[Thread]:
3334
return await storage.list_threads(opengpts_user_id)
3435

3536

36-
@router.get("/{tid}/messages")
37-
async def get_thread_messages(
37+
@router.get("/{tid}/state")
38+
async def get_thread_state(
3839
opengpts_user_id: OpengptsUserId,
3940
tid: ThreadID,
4041
):
4142
"""Get all messages for a thread."""
42-
return await storage.get_thread_messages(opengpts_user_id, tid)
43+
return await storage.get_thread_state(opengpts_user_id, tid)
4344

4445

45-
@router.post("/{tid}/messages")
46-
async def add_thread_messages(
46+
@router.post("/{tid}/state")
47+
async def update_thread_state(
48+
payload: ThreadPostRequest,
4749
opengpts_user_id: OpengptsUserId,
4850
tid: ThreadID,
49-
payload: ThreadMessagesPostRequest,
5051
):
5152
"""Add messages to a thread."""
52-
return await storage.post_thread_messages(opengpts_user_id, tid, payload.messages)
53+
return await storage.update_thread_state(
54+
payload.config or {"configurable": {"thread_id": tid}}, payload.values
55+
)
5356

5457

5558
@router.get("/{tid}/history")

backend/app/storage.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from datetime import datetime, timezone
2-
from typing import List, Optional, Sequence
2+
from typing import Any, List, Optional, Sequence, Union
33

44
from langchain_core.messages import AnyMessage
5+
from langchain_core.runnables import RunnableConfig
56

67
from app.agent import AgentType, get_agent_executor
78
from app.lifespan import get_pg_pool
@@ -98,37 +99,36 @@ async def get_thread(user_id: str, thread_id: str) -> Optional[Thread]:
9899
)
99100

100101

101-
async def get_thread_messages(user_id: str, thread_id: str):
102+
async def get_thread_state(user_id: str, thread_id: str):
102103
"""Get all messages for a thread."""
103104
app = get_agent_executor([], AgentType.GPT_35_TURBO, "", False)
104105
state = await app.aget_state({"configurable": {"thread_id": thread_id}})
105106
return {
106-
"messages": state.values,
107-
"resumeable": bool(state.next),
107+
"values": state.values,
108+
"next": state.next,
108109
}
109110

110111

111-
async def post_thread_messages(
112-
user_id: str, thread_id: str, messages: Sequence[AnyMessage]
112+
async def update_thread_state(
113+
config: RunnableConfig, messages: Union[Sequence[AnyMessage], dict[str, Any]]
113114
):
114115
"""Add messages to a thread."""
115116
app = get_agent_executor([], AgentType.GPT_35_TURBO, "", False)
116-
await app.aupdate_state({"configurable": {"thread_id": thread_id}}, messages)
117+
return await app.aupdate_state(config, messages)
117118

118119

119120
async def get_thread_history(user_id: str, thread_id: str):
120121
"""Get the history of a thread."""
121122
app = get_agent_executor([], AgentType.GPT_35_TURBO, "", False)
123+
config = {"configurable": {"thread_id": thread_id}}
122124
return [
123125
{
124126
"values": c.values,
125-
"resumeable": bool(c.next),
127+
"next": c.next,
126128
"config": c.config,
127129
"parent": c.parent_config,
128130
}
129-
async for c in app.aget_state_history(
130-
{"configurable": {"thread_id": thread_id}}
131-
)
131+
async for c in app.aget_state_history(config)
132132
]
133133

134134

backend/tests/unit_tests/app/test_app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ async def test_threads() -> None:
110110
)
111111
assert response.status_code == 200, response.text
112112

113-
response = await client.get(f"/threads/{tid}/messages", headers=headers)
113+
response = await client.get(f"/threads/{tid}/state", headers=headers)
114114
assert response.status_code == 200
115-
assert response.json() == {"messages": [], "resumeable": False}
115+
assert response.json() == {"values": [], "resumeable": False}
116116

117117
response = await client.get("/threads/", headers=headers)
118118

frontend/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "frontend",
33
"private": true,
44
"version": "0.0.0",
5+
"packageManager": "yarn@1.22.19",
56
"type": "module",
67
"scripts": {
78
"dev": "vite --host",
@@ -11,9 +12,12 @@
1112
"format": "prettier -w src"
1213
},
1314
"dependencies": {
15+
"@emotion/react": "^11.11.4",
16+
"@emotion/styled": "^11.11.0",
1417
"@headlessui/react": "^1.7.17",
1518
"@heroicons/react": "^2.0.18",
1619
"@microsoft/fetch-event-source": "^2.0.1",
20+
"@mui/material": "^5.15.14",
1721
"@tailwindcss/forms": "^0.5.6",
1822
"@tailwindcss/typography": "^0.5.10",
1923
"clsx": "^2.0.0",

frontend/src/App.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ function App(props: { edit?: boolean }) {
2727
const { currentChat, assistantConfig, isLoading } = useThreadAndAssistant();
2828

2929
const startTurn = useCallback(
30-
async (message: MessageWithFiles | null, thread_id: string) => {
30+
async (
31+
message: MessageWithFiles | null,
32+
thread_id: string,
33+
config?: Record<string, unknown>,
34+
) => {
3135
const files = message?.files || [];
3236
if (files.length > 0) {
3337
const formData = files.reduce((formData, file) => {
@@ -56,6 +60,7 @@ function App(props: { edit?: boolean }) {
5660
]
5761
: null,
5862
thread_id,
63+
config,
5964
);
6065
},
6166
[startStream],

frontend/src/assets/EmptyState.svg

+21
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Ref } from "react";
2+
import { cn } from "../utils/cn";
3+
4+
const COMMON_CLS = cn(
5+
"text-sm col-[1] row-[1] m-0 resize-none overflow-hidden whitespace-pre-wrap break-words bg-transparent px-2 py-1 rounded shadow-none",
6+
);
7+
8+
export function AutosizeTextarea(props: {
9+
id?: string;
10+
inputRef?: Ref<HTMLTextAreaElement>;
11+
value?: string | null | undefined;
12+
placeholder?: string;
13+
className?: string;
14+
onChange?: (e: string) => void;
15+
onFocus?: () => void;
16+
onBlur?: () => void;
17+
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
18+
autoFocus?: boolean;
19+
readOnly?: boolean;
20+
cursorPointer?: boolean;
21+
disabled?: boolean;
22+
fullHeight?: boolean;
23+
}) {
24+
return (
25+
<div
26+
className={
27+
cn("grid w-full", props.className) +
28+
(props.fullHeight ? "" : " max-h-80 overflow-auto ")
29+
}
30+
>
31+
<textarea
32+
ref={props.inputRef}
33+
id={props.id}
34+
className={cn(
35+
COMMON_CLS,
36+
"text-transparent caret-black rounded focus:outline-0 focus:ring-0",
37+
)}
38+
disabled={props.disabled}
39+
value={props.value ?? ""}
40+
rows={1}
41+
onChange={(e) => {
42+
const target = e.target as HTMLTextAreaElement;
43+
props.onChange?.(target.value);
44+
}}
45+
onFocus={props.onFocus}
46+
onBlur={props.onBlur}
47+
placeholder={props.placeholder}
48+
readOnly={props.readOnly}
49+
autoFocus={props.autoFocus && !props.readOnly}
50+
onKeyDown={props.onKeyDown}
51+
/>
52+
<div
53+
aria-hidden
54+
className={cn(COMMON_CLS, "pointer-events-none select-none")}
55+
>
56+
{props.value}{" "}
57+
</div>
58+
</div>
59+
);
60+
}

frontend/src/components/Chat.tsx

+13-5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ import { ArrowDownCircleIcon } from "@heroicons/react/24/outline";
77
import { MessageWithFiles } from "../utils/formTypes.ts";
88
import { useParams } from "react-router-dom";
99
import { useThreadAndAssistant } from "../hooks/useThreadAndAssistant.ts";
10+
// import { useHistories } from "../hooks/useHistories.ts";
11+
// import { Timeline } from "./Timeline.tsx";
12+
// import { deepEquals } from "../utils/equals.ts";
1013

11-
interface ChatProps extends Pick<StreamStateProps, "stream" | "stopStream"> {
14+
interface ChatProps
15+
extends Pick<
16+
StreamStateProps,
17+
"stream" | "stopStream" | "streamErrorMessage"
18+
> {
1219
startStream: (
1320
message: MessageWithFiles | null,
1421
thread_id: string,
@@ -26,7 +33,7 @@ function usePrevious<T>(value: T): T | undefined {
2633

2734
export function Chat(props: ChatProps) {
2835
const { chatId } = useParams();
29-
const { messages, resumeable } = useChatMessages(
36+
const { messages, next } = useChatMessages(
3037
chatId ?? null,
3138
props.stream,
3239
props.stopStream,
@@ -67,12 +74,13 @@ export function Chat(props: ChatProps) {
6774
...
6875
</div>
6976
)}
70-
{props.stream?.status === "error" && (
77+
{(props.streamErrorMessage || props.stream?.status === "error") && (
7178
<div className="flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20">
72-
An error has occurred. Please try again.
79+
{props.streamErrorMessage ??
80+
"An error has occurred. Please try again."}
7381
</div>
7482
)}
75-
{resumeable && props.stream?.status !== "inflight" && (
83+
{next.length && props.stream?.status !== "inflight" && (
7684
<div
7785
className="flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-800 ring-1 ring-inset ring-yellow-600/20 cursor-pointer"
7886
onClick={() =>

0 commit comments

Comments
 (0)