reactor.py: fix llm_call() silently skipping fallback for explicit provider requests, add /comms/sent/{id}/send endpoint for approving queued drafts

This commit is contained in:
root
2026-07-07 08:58:36 -05:00
parent 5ff4f3e311
commit 48b2a8523a
+49 -10
View File
@@ -130,18 +130,29 @@ async def handle_shell(payload: dict) -> dict:
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
async def llm_call(messages: list, provider: str = "claude", system: str = "") -> str: async def llm_call(messages: list, provider: str = "claude", system: str = "") -> str:
if provider == "claude" and CLAUDE_API_KEY: # Fixed 2026-07-07: previously, an explicitly-requested provider (e.g. "claude")
return await _claude_call(messages, system) # called its API directly with no exception handling, so any failure (rate limit,
elif provider == "groq" and GROQ_API_KEY: # depleted credits, outage) raised straight up instead of falling back to the other
return await _groq_call(messages, system) # providers below. The fallback loop only ever ran for an unrecognized provider
elif provider == "ollama": # string, which never happens in practice — so callers requesting "claude" got zero
return await _ollama_call(messages, system) # real resilience. Now every explicit request tries its provider first, then falls
for p in ["groq", "ollama"]: # through the remaining ones in order before giving up.
order = {"claude": ["claude", "groq", "ollama"],
"groq": ["groq", "ollama"],
"ollama": ["ollama"]}.get(provider, ["claude", "groq", "ollama"])
last_err = None
for p in order:
try: try:
return await llm_call(messages, p, system) if p == "claude" and CLAUDE_API_KEY:
except Exception: return await _claude_call(messages, system)
elif p == "groq" and GROQ_API_KEY:
return await _groq_call(messages, system)
elif p == "ollama":
return await _ollama_call(messages, system)
except Exception as e:
last_err = e
continue continue
raise RuntimeError("All LLM providers failed") raise RuntimeError(f"All LLM providers failed (last error: {last_err})")
async def _claude_call(messages: list, system: str = "") -> str: async def _claude_call(messages: list, system: str = "") -> str:
payload = {"model": CLAUDE_MODEL, "max_tokens": 4096, "messages": messages} payload = {"model": CLAUDE_MODEL, "max_tokens": 4096, "messages": messages}
@@ -2785,5 +2796,33 @@ async def comms_sent_delete(sent_id: int):
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,)) await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
return {"ok": True} return {"ok": True}
@app.post("/comms/sent/{sent_id}/send")
async def comms_sent_send(sent_id: int):
"""
Added 2026-07-07: sends a previously-composed queued draft. Compose always
created a draft (status='queued') for review, but there was no action anywhere
to actually send one — this closes that gap. handle_send_email() records its own
fresh row in email_sent (sent/failed), so the original queued draft row is removed
here once we've handed its content off, rather than leaving a stale duplicate.
"""
row = await db_fetchone(
"SELECT account, to_email, to_name, subject, body, triage_id FROM email_sent WHERE id=%s AND status='queued'",
(sent_id,)
)
if not row:
raise HTTPException(status_code=404, detail="Queued draft not found")
result = await handle_send_email({
"account": row["account"],
"to_email": row["to_email"],
"to_name": row["to_name"],
"subject": row["subject"],
"body": row["body"],
"triage_id": row["triage_id"],
})
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
return result
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("reactor:app", host=HOST, port=PORT, log_level="info", access_log=False) uvicorn.run("reactor:app", host=HOST, port=PORT, log_level="info", access_log=False)