Files
letta-server/memgpt/server/rest_api/server.py
Robin Goetz a68e2c838d feat: adding first poc of web UI (#625)
* updated local APIs to return usage info (#585)

* updated APIs to return usage info

* tested all endpoints

* added autogen as an extra (#616)

* added autogen as an extra

* updated docs

Co-authored-by: hemanthsavasere <hemanth.savasere@gmail.com>

* Update LICENSE

* Add safeguard on tokens returned by functions (#576)

* swapping out hardcoded str for prefix (forgot to include in #569)

* add extra failout when the summarizer tries to run on a single message

* added function response validation code, currently will truncate responses based on character count

* added return type hints (functions/tools should either return strings or None)

* discuss function output length in custom function section

* made the truncation more informative

* patch bug where None.copy() throws runtime error (#617)

* allow passing custom host to uvicorn (#618)

* feat: initial poc for socket server

* feat: initial poc for frontend based on react

Set up an nx workspace which maks it easy to manage dependencies and added shadcn components
that allow us to build good-looking ui in a fairly simple way.
UI is a very simple and basic chat that starts with a message of the user and then simply displays the
answer string that is sent back from the fastapi ws endpoint

* feat: mapp arguments to json and return new messages

Except for the previous user message we return all newly generated messages and let the frontend figure out how to display them.

* feat: display messages based on role and show inner thoughts and connection status

* chore: build newest frontend

* feat(frontend): show loader while waiting for first message and disable send button until connection is open

* feat: make agent send the first message and loop similar to CLI

currently the CLI loops until the correct function call sends a message to the user. this is an initial try to achieve a similar behavior in the socket server

* chore: build new version of frontend

* fix: rename lib directory so it is not excluded as part of python gitignore

* chore: rebuild frontend app

* fix: save agent at end of each response to allow the conversation to carry on over multiple sessions

* feat: restructure server to support multiple endpoints and add agents and sources endpoint

* feat: setup frontend routing and settings page

* chore: build frontend

* feat: another iteration of web interface

changes include: websocket for chat. switching between different agents. introduction of zustand state management

* feat: adjust frontend to work with memgpt rest-api

* feat: adjust existing rest_api to serve and interact with frontend

* feat: build latest frontend

* chore: build latest frontend

* fix: cleanup workspace

---------

Co-authored-by: Charles Packer <packercharles@gmail.com>
Co-authored-by: hemanthsavasere <hemanth.savasere@gmail.com>
2024-01-11 14:47:51 +01:00

202 lines
5.6 KiB
Python

import asyncio
import os
from contextlib import asynccontextmanager
import json
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.cors import CORSMiddleware
from starlette.staticfiles import StaticFiles
from memgpt.server.server import SyncServer
from memgpt.server.rest_api.interface import QueuingInterface
"""
Basic REST API sitting on top of the internal MemGPT python server (SyncServer)
Start the server with:
cd memgpt/server/rest_api
poetry run uvicorn server:app --reload
"""
class CreateAgentConfig(BaseModel):
user_id: str
config: dict
class UserMessage(BaseModel):
user_id: str
agent_id: str
message: str
stream: bool = False
class Command(BaseModel):
user_id: str
agent_id: str
command: str
class CoreMemory(BaseModel):
user_id: str
agent_id: str
human: str | None = None
persona: str | None = None
class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except (HTTPException, StarletteHTTPException) as ex:
if ex.status_code == 404:
return await super().get_response("index.html", scope)
else:
raise ex
server: SyncServer | None = None
interface: QueuingInterface | None = None
@asynccontextmanager
async def lifespan(application: FastAPI):
global server
global interface
interface = QueuingInterface()
server = SyncServer(default_interface=interface)
yield
server.save_agents()
server = None
CORS_ORIGINS = [
"http://localhost:4200",
"http://localhost:4201",
"http://localhost:8283",
"http://127.0.0.1:4200",
"http://127.0.0.1:4201",
"http://127.0.0.1:8283",
]
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/agents")
def list_agents(user_id: str):
interface.clear()
return server.list_agents(user_id=user_id)
@app.get("/agents/memory")
def get_agent_memory(user_id: str, agent_id: str):
interface.clear()
return server.get_agent_memory(user_id=user_id, agent_id=agent_id)
@app.put("/agents/memory")
def put_agent_memory(body: CoreMemory):
interface.clear()
new_memory_contents = {"persona": body.persona, "human": body.human}
return server.update_agent_core_memory(user_id=body.user_id, agent_id=body.agent_id, new_memory_contents=new_memory_contents)
@app.get("/agents/config")
def get_agent_config(user_id: str, agent_id: str):
interface.clear()
return server.get_agent_config(user_id=user_id, agent_id=agent_id)
@app.get("/config")
def get_server_config(user_id: str):
interface.clear()
return server.get_server_config(user_id=user_id)
# server.create_agent
@app.post("/agents")
def create_agents(body: CreateAgentConfig):
interface.clear()
try:
agent_id = server.create_agent(user_id=body.user_id, agent_config=body.config)
except Exception as e:
raise HTTPException(status_code=500, detail=f"{e}")
return {"agent_id": agent_id}
# server.user_message
@app.post("/agents/message")
async def user_message(body: UserMessage):
if body.stream:
# For streaming response
try:
# Start the generation process (similar to the non-streaming case)
# This should be a non-blocking call or run in a background task
# Check if server.user_message is an async function
if asyncio.iscoroutinefunction(server.user_message):
# Start the async task
await asyncio.create_task(server.user_message(user_id=body.user_id, agent_id=body.agent_id, message=body.message))
else:
# Run the synchronous function in a thread pool
loop = asyncio.get_event_loop()
loop.run_in_executor(None, server.user_message, body.user_id, body.agent_id, body.message)
async def formatted_message_generator():
async for message in interface.message_generator():
formatted_message = f"data: {json.dumps(message)}\n\n"
yield formatted_message
await asyncio.sleep(1)
# Return the streaming response using the generator
return StreamingResponse(formatted_message_generator(), media_type="text/event-stream")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"{e}")
else:
interface.clear()
try:
server.user_message(user_id=body.user_id, agent_id=body.agent_id, message=body.message)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"{e}")
return {"messages": interface.to_list()}
# server.run_command
@app.post("/agents/command")
def run_command(body: Command):
interface.clear()
try:
response = server.run_command(user_id=body.user_id, agent_id=body.agent_id, command=body.command)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"{e}")
return {"response": response}
app.mount(
"/",
SPAStaticFiles(
directory=os.path.join(os.getcwd(), "..", "static_files"),
html=True,
),
name="spa-static-files",
)