feat: chat sidebar and inbox, feat: saved chats, fix: wake jobs, fix: sandbox sends, ux: negotiation replies and draft box

This commit is contained in:
2026-05-06 22:53:19 -04:00
parent 58a57ddc6a
commit 3b6e3c34d5
18 changed files with 1797 additions and 105 deletions
+79 -3
View File
@@ -21,7 +21,7 @@ from pydantic import BaseModel
from traderai.agent import OllamaAgent, OllamaUnavailable
from traderai.config import save_settings, settings_payload
from traderai.config import get_settings
from traderai.memory import MemoryStore
from traderai.memory import DEFAULT_THREAD_ID, MemoryStore
from traderai.scheduler import WakeScheduler
from traderai.tools import ToolRegistry
from traderai.uex_client import UEXClient
@@ -35,6 +35,19 @@ def resource_path(*parts: str) -> Path:
class ChatRequest(BaseModel):
message: str
thread_id: str | None = DEFAULT_THREAD_ID
class ChatThreadRequest(BaseModel):
title: str | None = None
class RenameChatThreadRequest(BaseModel):
title: str
class DirectNegotiationMessageRequest(BaseModel):
message: str
class ClearMemoryRequest(BaseModel):
@@ -246,18 +259,43 @@ def create_app() -> FastAPI:
@app.post("/api/chat")
async def chat(request: ChatRequest) -> dict:
try:
return await agent.chat(request.message)
return await agent.chat(request.message, thread_id=request.thread_id)
except OllamaUnavailable as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest) -> StreamingResponse:
async def events():
async for event in agent.chat_events(request.message):
async for event in agent.chat_events(request.message, thread_id=request.thread_id):
yield f"data: {json.dumps(event)}\n\n"
return StreamingResponse(events(), media_type="text/event-stream")
@app.get("/api/chats")
async def chats() -> dict:
return {"chats": memory.list_threads()}
@app.post("/api/chats")
async def create_chat(request: ChatThreadRequest) -> dict:
return {"chat": memory.create_thread(request.title)}
@app.get("/api/chats/{thread_id}/messages")
async def chat_messages(thread_id: str) -> dict:
memory.ensure_thread(thread_id)
return {"thread_id": thread_id, "messages": memory.recent_conversation(limit=200, thread_id=thread_id)}
@app.delete("/api/chats/{thread_id}")
async def delete_chat(thread_id: str) -> dict:
deleted = memory.delete_thread(thread_id)
return {"deleted": deleted, "chats": memory.list_threads()}
@app.patch("/api/chats/{thread_id}")
async def rename_chat(thread_id: str, request: RenameChatThreadRequest) -> dict:
chat = memory.rename_thread(thread_id, request.title)
if not chat:
raise HTTPException(status_code=400, detail="A non-empty chat title is required.")
return {"chat": chat, "chats": memory.list_threads()}
@app.get("/api/pending-actions")
async def pending_actions() -> dict:
return {"pending_actions": agent._pending_payloads()}
@@ -266,6 +304,35 @@ def create_app() -> FastAPI:
async def notifications() -> dict:
return {"notifications": memory.undelivered_outbox()}
@app.get("/api/inbox")
async def inbox() -> dict:
return {"inbox": memory.list_outbox()}
@app.post("/api/inbox/{inbox_id}/continue")
async def continue_inbox(inbox_id: int) -> dict:
item = memory.get_outbox(inbox_id)
if not item:
raise HTTPException(status_code=404, detail="Inbox item not found.")
thread = memory.create_thread("Inbox follow-up")
memory.add_conversation("assistant", item["content"], thread["id"])
return {"chat": thread, "message": item}
@app.delete("/api/inbox/{inbox_id}")
async def delete_inbox(inbox_id: int) -> dict:
deleted = memory.delete_outbox(inbox_id)
return {"deleted": deleted, "inbox": memory.list_outbox()}
@app.get("/api/negotiations/{identifier}/messages")
async def negotiation_messages(identifier: str) -> dict:
params = negotiation_identifier_params(identifier)
return await uex.get("marketplace_negotiations_messages", params, authenticated=True)
@app.post("/api/negotiations/{identifier}/messages")
async def send_negotiation_message(identifier: str, request: DirectNegotiationMessageRequest) -> dict:
params = negotiation_identifier_params(identifier)
payload = {**params, "message": request.message, "is_production": 1}
return await uex.post("marketplace_negotiations_messages", payload, authenticated=True)
@app.get("/api/wake-jobs")
async def wake_jobs() -> dict:
return {"scheduled_jobs": scheduler.list_jobs()}
@@ -300,6 +367,15 @@ def create_app() -> FastAPI:
return app
def negotiation_identifier_params(identifier: str) -> dict[str, Any]:
value = identifier.strip()
if not value:
raise HTTPException(status_code=400, detail="Negotiation id or hash is required.")
if value.isdigit():
return {"id_negotiation": int(value)}
return {"hash": value}
async def inspect_ollama() -> dict[str, Any]:
settings = get_settings()
executable = find_ollama_executable()