feat: chat

This commit is contained in:
2026-06-09 11:24:15 -04:00
parent 454bb57484
commit 8fac3d2bae
15 changed files with 2015 additions and 38 deletions
+159 -12
View File
@@ -24,6 +24,7 @@ from traderai.config import save_settings, settings_payload
from traderai.config import get_settings
from traderai.cornerstone_client import CornerstoneClient
from traderai.memory import DEFAULT_THREAD_ID, MemoryStore
from traderai.negotiations import NegotiationSyncService
from traderai.plans import ContinualPlanRunner, ContinualPlanStore
from traderai.scheduler import WakeScheduler
from traderai.scmdb_client import SCMDBClient
@@ -63,6 +64,21 @@ class DirectNegotiationMessageRequest(BaseModel):
message: str
class NegotiationDraftMessageRequest(BaseModel):
message: str
class NegotiationCloseRequest(BaseModel):
deal_closed: bool
deal_value: float | None = None
currency: str | None = None
clarity_rating: int | None = None
speed_rating: int | None = None
respect_rating: int | None = None
fairness_rating: int | None = None
comment: str | None = None
class ClearMemoryRequest(BaseModel):
include_memories: bool = True
include_conversations: bool = True
@@ -120,22 +136,42 @@ def create_app() -> FastAPI:
runtime: dict[str, Any] = {}
def configure_runtime(current_settings: Any) -> None:
uex = UEXClient(current_settings.uex_base_url, current_settings.uex_secret_key, current_settings.uex_bearer_token)
uex = UEXClient(
current_settings.uex_base_url,
current_settings.uex_secret_key,
current_settings.uex_bearer_token,
negotiation_close_endpoint=current_settings.uex_negotiation_close_endpoint,
)
negotiation_sync = NegotiationSyncService(memory, uex)
scmdb = SCMDBClient(current_settings.scmdb_base_url)
cornerstone = CornerstoneClient(current_settings.cornerstone_base_url)
scwiki = StarCitizenWikiClient(current_settings.scwiki_base_url, current_settings.scwiki_api_base_url)
wikelo = WikeloProjectsClient()
tools = ToolRegistry(
uex,
current_settings.require_write_approval,
memory=memory,
scheduler=scheduler,
scmdb=scmdb,
cornerstone=cornerstone,
scwiki=scwiki,
wikelo=wikelo,
plan_store=plan_store,
)
try:
tools = ToolRegistry(
uex,
current_settings.require_write_approval,
memory=memory,
scheduler=scheduler,
scmdb=scmdb,
cornerstone=cornerstone,
scwiki=scwiki,
wikelo=wikelo,
plan_store=plan_store,
negotiation_sync=negotiation_sync,
)
except TypeError:
tools = ToolRegistry(
uex,
current_settings.require_write_approval,
memory=memory,
scheduler=scheduler,
scmdb=scmdb,
cornerstone=cornerstone,
scwiki=scwiki,
wikelo=wikelo,
plan_store=plan_store,
)
plan_runner = ContinualPlanRunner(plan_store, tools, memory)
tools.plan_runner = plan_runner
provider_base_url, provider_model, provider_api_key = provider_settings(current_settings)
@@ -154,6 +190,8 @@ def create_app() -> FastAPI:
scheduler.bind_agent(agent)
scheduler.bind_plan_runner(plan_runner)
scheduler.bind_uex_notifications(uex, current_settings.uex_notification_poll_seconds)
if hasattr(scheduler, "bind_negotiation_sync"):
scheduler.bind_negotiation_sync(negotiation_sync)
runtime.update(
{
"settings": current_settings,
@@ -161,6 +199,7 @@ def create_app() -> FastAPI:
"tools": tools,
"plan_runner": plan_runner,
"agent": agent,
"negotiation_sync": negotiation_sync,
}
)
@@ -173,6 +212,10 @@ def create_app() -> FastAPI:
@app.on_event("startup")
async def startup() -> None:
await refresh_user_profile()
try:
await runtime["negotiation_sync"].startup_sync()
except Exception:
memory.set_profile("uex_last_negotiation_sync_error", "startup_sync_failed")
scheduler.start()
@app.on_event("shutdown")
@@ -468,8 +511,78 @@ def create_app() -> FastAPI:
deleted = memory.delete_outbox(inbox_id)
return {"deleted": deleted, "inbox": memory.list_outbox()}
@app.get("/api/negotiations")
async def negotiations(status: str = "all", unread_only: bool = False, search: str = "", limit: int = 50) -> dict:
negotiation_sync = runtime["negotiation_sync"]
return {
"negotiations": negotiation_sync.list_negotiations(
status=status,
unread_only=unread_only,
search=search,
limit=limit,
)
}
@app.get("/api/negotiations/unread-count")
async def negotiations_unread_count() -> dict:
return {"unread_count": runtime["negotiation_sync"].unread_count()}
@app.post("/api/negotiations/refresh-all")
async def negotiations_refresh_all() -> dict:
result = await runtime["negotiation_sync"].refresh_negotiations(seed_open_messages=True)
return result
@app.get("/api/negotiations/{identifier}")
async def negotiation_detail(identifier: str) -> dict:
negotiation_sync = runtime["negotiation_sync"]
negotiation = negotiation_sync.get_negotiation(identifier, mark_read=True)
if not negotiation:
raise HTTPException(status_code=404, detail="Negotiation not found.")
return {"negotiation": negotiation}
@app.post("/api/negotiations/{identifier}/refresh")
async def refresh_negotiation(identifier: str) -> dict:
negotiation_sync = runtime["negotiation_sync"]
result = await negotiation_sync.refresh_negotiation(identifier, mark_read=False)
return {"negotiation": negotiation_sync.get_negotiation(identifier, mark_read=False), "refreshed": result.refreshed}
@app.post("/api/negotiations/{identifier}/open-chat")
async def open_negotiation_chat(identifier: str) -> dict:
negotiation_sync = runtime["negotiation_sync"]
negotiation = negotiation_sync.get_negotiation(identifier, mark_read=False)
if not negotiation:
raise HTTPException(status_code=404, detail="Negotiation not found.")
thread = memory.create_thread(negotiation.get("title") or f"Negotiation {identifier}")
context = {
"hash": negotiation.get("hash"),
"title": negotiation.get("title"),
"counterparty_username": negotiation.get("counterparty_username"),
"status": negotiation.get("status"),
"unread_count": negotiation.get("unread_count"),
"last_message_at": negotiation.get("last_message_at"),
"recent_messages": [
{
"author_username": item.get("author_username"),
"is_me": item.get("is_me"),
"body": item.get("body"),
"sent_at": item.get("sent_at"),
}
for item in (negotiation.get("messages") or [])[-8:]
],
}
memory.add_conversation(
"assistant",
"Negotiation context loaded:\n" + json.dumps(context, ensure_ascii=True, indent=2),
thread["id"],
)
return {"chat": thread, "negotiation": negotiation}
@app.get("/api/negotiations/{identifier}/messages")
async def negotiation_messages(identifier: str) -> dict:
negotiation_sync = runtime["negotiation_sync"]
negotiation = negotiation_sync.get_negotiation(identifier, mark_read=True)
if negotiation:
return {"messages": negotiation.get("messages", []), "negotiation": negotiation}
uex = runtime["uex"]
params = negotiation_identifier_params(identifier)
return await uex.get("marketplace_negotiations_messages", params, authenticated=True)
@@ -481,6 +594,40 @@ def create_app() -> FastAPI:
payload = {**params, "message": request.message, "is_production": 1}
return await uex.post("marketplace_negotiations_messages", payload, authenticated=True)
@app.post("/api/negotiations/{identifier}/messages/manual")
async def send_negotiation_message_manual(identifier: str, request: DirectNegotiationMessageRequest) -> dict:
result = await runtime["negotiation_sync"].manual_send_message(identifier, request.message)
return {
**result,
"message": "Sent",
"negotiation": runtime["negotiation_sync"].get_negotiation(identifier, mark_read=True),
}
@app.post("/api/negotiations/{identifier}/messages/draft")
async def draft_negotiation_message(identifier: str, request: NegotiationDraftMessageRequest) -> dict:
tools = runtime["tools"]
result = await tools.draft_negotiation_message(hash=identifier, message=request.message)
if result.get("error"):
raise HTTPException(status_code=400, detail=result["error"])
return result
@app.post("/api/negotiations/{identifier}/close/draft")
async def draft_negotiation_close(identifier: str, request: NegotiationCloseRequest) -> dict:
tools = runtime["tools"]
result = await tools.draft_negotiation_close(hash=identifier, **request.model_dump())
if result.get("error"):
raise HTTPException(status_code=400, detail=result["error"])
return result
@app.post("/api/negotiations/{identifier}/close/manual")
async def close_negotiation_manual(identifier: str, request: NegotiationCloseRequest) -> dict:
result = await runtime["negotiation_sync"].manual_close_negotiation(identifier, request.model_dump())
return {
**result,
"message": "Deal submitted",
"negotiation": runtime["negotiation_sync"].get_negotiation(identifier, mark_read=True),
}
@app.get("/api/wake-jobs")
async def wake_jobs() -> dict:
return {"scheduled_jobs": scheduler.list_jobs()}