Spaces:
Paused
Paused
Update emergent2api v0.2.0: auth_token, admin endpoints, account management
Browse files- Default auth_token: sk-6loA0HwMQP1mdhPvI
- Add JSONL import, toggle, delete, JWT refresh endpoints
- Update app_version to 3.5.0
- Add python-multipart dependency
Made-with: Cursor
- emergent2api/__init__.py +1 -0
- emergent2api/app.py +110 -21
- emergent2api/backends/__init__.py +1 -0
- emergent2api/backends/jobs.py +79 -51
- emergent2api/config.py +2 -2
- emergent2api/database.py +31 -0
- emergent2api/routes/__init__.py +1 -0
- requirements.txt +2 -0
emergent2api/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
emergent2api/app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Emergent2API — OpenAI/Anthropic-compatible API gateway for Emergent.sh accounts."""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
|
|
|
| 4 |
import logging
|
| 5 |
import os
|
| 6 |
|
|
@@ -100,17 +101,27 @@ async def root():
|
|
| 100 |
count = await pool.count()
|
| 101 |
return {
|
| 102 |
"service": "Emergent2API",
|
| 103 |
-
"version": "0.
|
| 104 |
"backend": settings.backend,
|
| 105 |
"accounts": count,
|
| 106 |
-
"endpoints":
|
| 107 |
-
"
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
| 115 |
|
| 116 |
|
|
@@ -125,8 +136,12 @@ async def health():
|
|
| 125 |
# ---------------------------------------------------------------------------
|
| 126 |
|
| 127 |
@app.get("/admin/accounts")
|
| 128 |
-
async def admin_list_accounts():
|
| 129 |
-
accounts
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
return {
|
| 131 |
"total": len(accounts),
|
| 132 |
"accounts": [
|
|
@@ -148,27 +163,101 @@ async def admin_add_account(request: Request):
|
|
| 148 |
"""Add an account manually. Body: {email, password, jwt, refresh_token?, user_id?, balance?}"""
|
| 149 |
body = await request.json()
|
| 150 |
required = ["email", "password", "jwt"]
|
| 151 |
-
for
|
| 152 |
-
if
|
| 153 |
-
return JSONResponse(
|
| 154 |
-
status_code=400,
|
| 155 |
-
content={"error": f"Missing required field: {field}"},
|
| 156 |
-
)
|
| 157 |
account_id = await db.upsert_account(body)
|
| 158 |
return {"id": account_id, "email": body["email"], "status": "added"}
|
| 159 |
|
| 160 |
|
| 161 |
@app.post("/admin/accounts/import")
|
| 162 |
async def admin_import_accounts(request: Request):
|
| 163 |
-
"""Import accounts from
|
| 164 |
body = await request.json()
|
| 165 |
accounts = body.get("accounts", [])
|
| 166 |
imported = 0
|
|
|
|
| 167 |
for acct in accounts:
|
| 168 |
if "email" in acct and "password" in acct and "jwt" in acct:
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
|
| 174 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1 |
"""Emergent2API — OpenAI/Anthropic-compatible API gateway for Emergent.sh accounts."""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
+
import json
|
| 5 |
import logging
|
| 6 |
import os
|
| 7 |
|
|
|
|
| 101 |
count = await pool.count()
|
| 102 |
return {
|
| 103 |
"service": "Emergent2API",
|
| 104 |
+
"version": "0.2.0",
|
| 105 |
"backend": settings.backend,
|
| 106 |
"accounts": count,
|
| 107 |
+
"endpoints": {
|
| 108 |
+
"api": [
|
| 109 |
+
"POST /v1/chat/completions (OpenAI)",
|
| 110 |
+
"POST /v1/messages (Anthropic)",
|
| 111 |
+
"POST /v1/responses (OpenAI Response API)",
|
| 112 |
+
"GET /v1/models",
|
| 113 |
+
],
|
| 114 |
+
"admin": [
|
| 115 |
+
"GET /admin/accounts",
|
| 116 |
+
"POST /admin/accounts",
|
| 117 |
+
"POST /admin/accounts/import",
|
| 118 |
+
"POST /admin/accounts/import-jsonl",
|
| 119 |
+
"DELETE /admin/accounts/{id}",
|
| 120 |
+
"POST /admin/accounts/{id}/toggle",
|
| 121 |
+
"POST /admin/accounts/{id}/refresh",
|
| 122 |
+
"POST /admin/accounts/refresh-all",
|
| 123 |
+
],
|
| 124 |
+
},
|
| 125 |
}
|
| 126 |
|
| 127 |
|
|
|
|
| 136 |
# ---------------------------------------------------------------------------
|
| 137 |
|
| 138 |
@app.get("/admin/accounts")
|
| 139 |
+
async def admin_list_accounts(active_only: bool = False):
|
| 140 |
+
"""List all accounts or only active ones."""
|
| 141 |
+
if active_only:
|
| 142 |
+
accounts = await db.get_active_accounts()
|
| 143 |
+
else:
|
| 144 |
+
accounts = await db.get_all_accounts()
|
| 145 |
return {
|
| 146 |
"total": len(accounts),
|
| 147 |
"accounts": [
|
|
|
|
| 163 |
"""Add an account manually. Body: {email, password, jwt, refresh_token?, user_id?, balance?}"""
|
| 164 |
body = await request.json()
|
| 165 |
required = ["email", "password", "jwt"]
|
| 166 |
+
for f in required:
|
| 167 |
+
if f not in body:
|
| 168 |
+
return JSONResponse(status_code=400, content={"error": f"Missing required field: {f}"})
|
|
|
|
|
|
|
|
|
|
| 169 |
account_id = await db.upsert_account(body)
|
| 170 |
return {"id": account_id, "email": body["email"], "status": "added"}
|
| 171 |
|
| 172 |
|
| 173 |
@app.post("/admin/accounts/import")
|
| 174 |
async def admin_import_accounts(request: Request):
|
| 175 |
+
"""Import accounts from JSON body. Body: {accounts: [{email, password, jwt, ...}, ...]}"""
|
| 176 |
body = await request.json()
|
| 177 |
accounts = body.get("accounts", [])
|
| 178 |
imported = 0
|
| 179 |
+
errors = []
|
| 180 |
for acct in accounts:
|
| 181 |
if "email" in acct and "password" in acct and "jwt" in acct:
|
| 182 |
+
try:
|
| 183 |
+
await db.upsert_account(acct)
|
| 184 |
+
imported += 1
|
| 185 |
+
except Exception as e:
|
| 186 |
+
errors.append({"email": acct.get("email"), "error": str(e)})
|
| 187 |
+
else:
|
| 188 |
+
errors.append({"email": acct.get("email", "?"), "error": "missing email/password/jwt"})
|
| 189 |
+
return {"imported": imported, "total_submitted": len(accounts), "errors": errors}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@app.post("/admin/accounts/import-jsonl")
|
| 193 |
+
async def admin_import_jsonl(request: Request):
|
| 194 |
+
"""Import accounts from JSONL text. Body: {jsonl: "line1\\nline2\\n..."} or raw JSONL body."""
|
| 195 |
+
content_type = request.headers.get("content-type", "")
|
| 196 |
+
if "application/json" in content_type:
|
| 197 |
+
body = await request.json()
|
| 198 |
+
raw_text = body.get("jsonl", "")
|
| 199 |
+
else:
|
| 200 |
+
raw_bytes = await request.body()
|
| 201 |
+
raw_text = raw_bytes.decode("utf-8", errors="replace")
|
| 202 |
+
lines = raw_text.strip().splitlines()
|
| 203 |
+
imported = 0
|
| 204 |
+
errors = []
|
| 205 |
+
for i, line in enumerate(lines):
|
| 206 |
+
line = line.strip()
|
| 207 |
+
if not line:
|
| 208 |
+
continue
|
| 209 |
+
try:
|
| 210 |
+
acct = json.loads(line)
|
| 211 |
+
if "email" in acct and "password" in acct and "jwt" in acct:
|
| 212 |
+
await db.upsert_account(acct)
|
| 213 |
+
imported += 1
|
| 214 |
+
else:
|
| 215 |
+
errors.append({"line": i + 1, "error": "missing email/password/jwt"})
|
| 216 |
+
except json.JSONDecodeError:
|
| 217 |
+
errors.append({"line": i + 1, "error": "invalid JSON"})
|
| 218 |
+
except Exception as e:
|
| 219 |
+
errors.append({"line": i + 1, "error": str(e)})
|
| 220 |
+
return {"imported": imported, "total_lines": len(lines), "errors": errors}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@app.delete("/admin/accounts/{account_id}")
|
| 224 |
+
async def admin_delete_account(account_id: int):
|
| 225 |
+
"""Permanently delete an account."""
|
| 226 |
+
await db.delete_account(account_id)
|
| 227 |
+
return {"id": account_id, "status": "deleted"}
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@app.post("/admin/accounts/{account_id}/toggle")
|
| 231 |
+
async def admin_toggle_account(account_id: int):
|
| 232 |
+
"""Toggle active/inactive status."""
|
| 233 |
+
new_state = await db.toggle_account(account_id)
|
| 234 |
+
return {"id": account_id, "is_active": new_state}
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
@app.post("/admin/accounts/{account_id}/refresh")
|
| 238 |
+
async def admin_refresh_jwt(account_id: int):
|
| 239 |
+
"""Refresh JWT for a specific account."""
|
| 240 |
+
account = await db.get_account_by_id(account_id)
|
| 241 |
+
if not account:
|
| 242 |
+
return JSONResponse(status_code=404, content={"error": "Account not found"})
|
| 243 |
+
new_jwt = await pool.refresh_jwt(account)
|
| 244 |
+
if new_jwt:
|
| 245 |
+
return {"id": account_id, "email": account["email"], "status": "refreshed"}
|
| 246 |
+
return JSONResponse(status_code=500, content={"error": "JWT refresh failed"})
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@app.post("/admin/accounts/refresh-all")
|
| 250 |
+
async def admin_refresh_all():
|
| 251 |
+
"""Refresh JWTs for all active accounts."""
|
| 252 |
+
accounts = await db.get_active_accounts()
|
| 253 |
+
results = {"refreshed": 0, "failed": 0, "total": len(accounts)}
|
| 254 |
+
for acct in accounts:
|
| 255 |
+
new_jwt = await pool.refresh_jwt(acct)
|
| 256 |
+
if new_jwt:
|
| 257 |
+
results["refreshed"] += 1
|
| 258 |
+
else:
|
| 259 |
+
results["failed"] += 1
|
| 260 |
+
return results
|
| 261 |
|
| 262 |
|
| 263 |
# ---------------------------------------------------------------------------
|
emergent2api/backends/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
emergent2api/backends/jobs.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
| 1 |
-
"""Jobs API backend: submit task + poll trajectory for responses.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import asyncio
|
|
@@ -15,6 +21,19 @@ from .base import EmergentBackend
|
|
| 15 |
|
| 16 |
logger = logging.getLogger("emergent2api.jobs")
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
class JobsBackend(EmergentBackend):
|
| 20 |
"""Wraps Emergent's Jobs API (submit-queue + trajectory polling) into a chat interface."""
|
|
@@ -28,11 +47,8 @@ class JobsBackend(EmergentBackend):
|
|
| 28 |
stream: bool = False,
|
| 29 |
) -> AsyncGenerator[dict[str, Any], None]:
|
| 30 |
jwt = account["jwt"]
|
| 31 |
-
|
| 32 |
-
# Flatten messages into a single task prompt
|
| 33 |
task = self._messages_to_task(messages)
|
| 34 |
|
| 35 |
-
# Submit job
|
| 36 |
ref_id = await asyncio.to_thread(
|
| 37 |
self._submit_job, jwt, task, model, thinking
|
| 38 |
)
|
|
@@ -42,66 +58,69 @@ class JobsBackend(EmergentBackend):
|
|
| 42 |
|
| 43 |
logger.info(f"Job submitted: ref_id={ref_id}, model={model}")
|
| 44 |
|
| 45 |
-
# Poll trajectory for results
|
| 46 |
full_text = ""
|
| 47 |
full_thinking = ""
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
|
|
|
| 55 |
result = await asyncio.to_thread(self._poll_trajectory, jwt, ref_id)
|
| 56 |
if result is None:
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
continue
|
| 60 |
continue
|
| 61 |
|
| 62 |
data_items = result.get("data", [])
|
| 63 |
-
job_status = result.get("status", "")
|
| 64 |
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
for item in data_items:
|
| 67 |
-
|
| 68 |
-
if
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
payload = item.get("traj_payload", {})
|
| 73 |
reasoning = payload.get("reasoning_content", "")
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
break
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
break
|
| 105 |
|
| 106 |
yield {
|
| 107 |
"type": "done",
|
|
@@ -109,6 +128,15 @@ class JobsBackend(EmergentBackend):
|
|
| 109 |
"thinking": full_thinking,
|
| 110 |
}
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def _submit_job(
|
| 113 |
self,
|
| 114 |
jwt: str,
|
|
|
|
| 1 |
+
"""Jobs API backend: submit task + poll trajectory for responses.
|
| 2 |
+
|
| 3 |
+
Streaming uses the "longest text" approach: each poll retrieves trajectory
|
| 4 |
+
items and picks the longest text across multiple possible fields. Delta text
|
| 5 |
+
(new chars since last poll) is yielded to the caller. End-of-stream is
|
| 6 |
+
detected via a double-confirmation mechanism (stable content for N cycles).
|
| 7 |
+
"""
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import asyncio
|
|
|
|
| 21 |
|
| 22 |
logger = logging.getLogger("emergent2api.jobs")
|
| 23 |
|
| 24 |
+
_TEXT_KEYS = ("thought", "output", "message", "content", "text", "response")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _extract_text_from_item(item: dict) -> str:
|
| 28 |
+
"""Try multiple fields to extract the longest text from a trajectory item."""
|
| 29 |
+
payload = item.get("traj_payload") or item
|
| 30 |
+
best = ""
|
| 31 |
+
for key in _TEXT_KEYS:
|
| 32 |
+
val = payload.get(key)
|
| 33 |
+
if val and isinstance(val, str) and len(val) > len(best):
|
| 34 |
+
best = val
|
| 35 |
+
return best
|
| 36 |
+
|
| 37 |
|
| 38 |
class JobsBackend(EmergentBackend):
|
| 39 |
"""Wraps Emergent's Jobs API (submit-queue + trajectory polling) into a chat interface."""
|
|
|
|
| 47 |
stream: bool = False,
|
| 48 |
) -> AsyncGenerator[dict[str, Any], None]:
|
| 49 |
jwt = account["jwt"]
|
|
|
|
|
|
|
| 50 |
task = self._messages_to_task(messages)
|
| 51 |
|
|
|
|
| 52 |
ref_id = await asyncio.to_thread(
|
| 53 |
self._submit_job, jwt, task, model, thinking
|
| 54 |
)
|
|
|
|
| 58 |
|
| 59 |
logger.info(f"Job submitted: ref_id={ref_id}, model={model}")
|
| 60 |
|
|
|
|
| 61 |
full_text = ""
|
| 62 |
full_thinking = ""
|
| 63 |
+
consecutive_unchanged = 0
|
| 64 |
+
confirmed_end = False
|
| 65 |
+
has_received_anything = False
|
| 66 |
+
max_wait_cycles = 400
|
| 67 |
|
| 68 |
+
# Initial wait to let the container spin up
|
| 69 |
+
await asyncio.sleep(8.0)
|
| 70 |
|
| 71 |
+
for _ in range(max_wait_cycles):
|
| 72 |
result = await asyncio.to_thread(self._poll_trajectory, jwt, ref_id)
|
| 73 |
if result is None:
|
| 74 |
+
await self._dynamic_sleep(consecutive_unchanged)
|
| 75 |
+
consecutive_unchanged += 1
|
|
|
|
| 76 |
continue
|
| 77 |
|
| 78 |
data_items = result.get("data", [])
|
|
|
|
| 79 |
|
| 80 |
+
# Find the longest text across all items (main content)
|
| 81 |
+
current_full_text = ""
|
| 82 |
+
current_thinking = ""
|
| 83 |
for item in data_items:
|
| 84 |
+
text = _extract_text_from_item(item)
|
| 85 |
+
if len(text) > len(current_full_text):
|
| 86 |
+
current_full_text = text
|
| 87 |
+
payload = item.get("traj_payload") or {}
|
|
|
|
|
|
|
| 88 |
reasoning = payload.get("reasoning_content", "")
|
| 89 |
+
if reasoning and len(reasoning) > len(current_thinking):
|
| 90 |
+
current_thinking = reasoning
|
| 91 |
+
|
| 92 |
+
# Yield thinking delta
|
| 93 |
+
if current_thinking and len(current_thinking) > len(full_thinking):
|
| 94 |
+
delta = current_thinking[len(full_thinking):]
|
| 95 |
+
full_thinking = current_thinking
|
| 96 |
+
if stream and delta:
|
| 97 |
+
yield {"type": "thinking", "content": delta}
|
| 98 |
+
|
| 99 |
+
# Yield text delta
|
| 100 |
+
if len(current_full_text) > len(full_text):
|
| 101 |
+
delta = current_full_text[len(full_text):]
|
| 102 |
+
full_text = current_full_text
|
| 103 |
+
consecutive_unchanged = 0
|
| 104 |
+
confirmed_end = False
|
| 105 |
+
has_received_anything = True
|
| 106 |
+
if stream and delta:
|
| 107 |
+
yield {"type": "text", "content": delta}
|
| 108 |
+
else:
|
| 109 |
+
consecutive_unchanged += 1
|
| 110 |
+
|
| 111 |
+
if has_received_anything and full_text and consecutive_unchanged >= 6:
|
| 112 |
+
if not confirmed_end:
|
| 113 |
+
logger.debug("Content stable, confirming end...")
|
| 114 |
+
confirmed_end = True
|
| 115 |
+
await asyncio.sleep(5.0)
|
| 116 |
+
continue
|
| 117 |
+
logger.info("Stream completed (double-confirmed stable)")
|
| 118 |
break
|
| 119 |
+
|
| 120 |
+
await self._dynamic_sleep(consecutive_unchanged)
|
| 121 |
+
|
| 122 |
+
if not has_received_anything:
|
| 123 |
+
logger.warning("Stream finished with NO content")
|
|
|
|
| 124 |
|
| 125 |
yield {
|
| 126 |
"type": "done",
|
|
|
|
| 128 |
"thinking": full_thinking,
|
| 129 |
}
|
| 130 |
|
| 131 |
+
@staticmethod
|
| 132 |
+
async def _dynamic_sleep(consecutive_unchanged: int) -> None:
|
| 133 |
+
if consecutive_unchanged == 0:
|
| 134 |
+
await asyncio.sleep(1.0)
|
| 135 |
+
elif consecutive_unchanged < 3:
|
| 136 |
+
await asyncio.sleep(2.0)
|
| 137 |
+
else:
|
| 138 |
+
await asyncio.sleep(3.0)
|
| 139 |
+
|
| 140 |
def _submit_job(
|
| 141 |
self,
|
| 142 |
jwt: str,
|
emergent2api/config.py
CHANGED
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
|
|
| 7 |
|
| 8 |
@dataclass
|
| 9 |
class Settings:
|
| 10 |
-
api_key: str = field(default_factory=lambda: os.environ.get("EMERGENT2API_KEY", "sk-
|
| 11 |
backend: str = field(default_factory=lambda: os.environ.get("EMERGENT_BACKEND", "jobs"))
|
| 12 |
proxy: str = field(default_factory=lambda: os.environ.get("EMERGENT_PROXY", ""))
|
| 13 |
poll_interval: float = field(default_factory=lambda: float(os.environ.get("EMERGENT_POLL_INTERVAL", "0.8")))
|
|
@@ -28,7 +28,7 @@ class Settings:
|
|
| 28 |
auth_base: str = "https://auth.emergent.sh/auth/v1"
|
| 29 |
api_base: str = "https://api.emergent.sh"
|
| 30 |
integrations_base: str = "https://integrations.emergentagent.com/llm"
|
| 31 |
-
app_version: str = "
|
| 32 |
|
| 33 |
|
| 34 |
settings = Settings()
|
|
|
|
| 7 |
|
| 8 |
@dataclass
|
| 9 |
class Settings:
|
| 10 |
+
api_key: str = field(default_factory=lambda: os.environ.get("EMERGENT2API_KEY", "sk-6loA0HwMQP1mdhPvI"))
|
| 11 |
backend: str = field(default_factory=lambda: os.environ.get("EMERGENT_BACKEND", "jobs"))
|
| 12 |
proxy: str = field(default_factory=lambda: os.environ.get("EMERGENT_PROXY", ""))
|
| 13 |
poll_interval: float = field(default_factory=lambda: float(os.environ.get("EMERGENT_POLL_INTERVAL", "0.8")))
|
|
|
|
| 28 |
auth_base: str = "https://auth.emergent.sh/auth/v1"
|
| 29 |
api_base: str = "https://api.emergent.sh"
|
| 30 |
integrations_base: str = "https://integrations.emergentagent.com/llm"
|
| 31 |
+
app_version: str = "3.5.0"
|
| 32 |
|
| 33 |
|
| 34 |
settings = Settings()
|
emergent2api/database.py
CHANGED
|
@@ -118,6 +118,37 @@ async def update_jwt(account_id: int, jwt: str, refresh_token: str = "") -> None
|
|
| 118 |
)
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
async def get_account_count() -> dict[str, int]:
|
| 122 |
pool = await get_pool()
|
| 123 |
async with pool.acquire() as conn:
|
|
|
|
| 118 |
)
|
| 119 |
|
| 120 |
|
| 121 |
+
async def get_all_accounts() -> list[dict[str, Any]]:
|
| 122 |
+
pool = await get_pool()
|
| 123 |
+
async with pool.acquire() as conn:
|
| 124 |
+
rows = await conn.fetch("SELECT * FROM emergent_accounts ORDER BY id ASC")
|
| 125 |
+
return [dict(r) for r in rows]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
async def get_account_by_id(account_id: int) -> Optional[dict[str, Any]]:
|
| 129 |
+
pool = await get_pool()
|
| 130 |
+
async with pool.acquire() as conn:
|
| 131 |
+
row = await conn.fetchrow("SELECT * FROM emergent_accounts WHERE id = $1", account_id)
|
| 132 |
+
return dict(row) if row else None
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
async def delete_account(account_id: int) -> None:
|
| 136 |
+
pool = await get_pool()
|
| 137 |
+
async with pool.acquire() as conn:
|
| 138 |
+
await conn.execute("DELETE FROM emergent_accounts WHERE id = $1", account_id)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
async def toggle_account(account_id: int) -> bool:
|
| 142 |
+
"""Toggle active status and return the new state."""
|
| 143 |
+
pool = await get_pool()
|
| 144 |
+
async with pool.acquire() as conn:
|
| 145 |
+
row = await conn.fetchrow(
|
| 146 |
+
"UPDATE emergent_accounts SET is_active = NOT is_active, updated_at = NOW() WHERE id = $1 RETURNING is_active",
|
| 147 |
+
account_id,
|
| 148 |
+
)
|
| 149 |
+
return row["is_active"] if row else False
|
| 150 |
+
|
| 151 |
+
|
| 152 |
async def get_account_count() -> dict[str, int]:
|
| 153 |
pool = await get_pool()
|
| 154 |
async with pool.acquire() as conn:
|
emergent2api/routes/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
requirements.txt
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
fastapi>=0.115
|
| 2 |
uvicorn[standard]>=0.30
|
|
|
|
| 3 |
pydantic-settings>=2.0
|
| 4 |
asyncpg>=0.29
|
| 5 |
httpx>=0.27
|
| 6 |
curl_cffi>=0.7
|
|
|
|
|
|
| 1 |
fastapi>=0.115
|
| 2 |
uvicorn[standard]>=0.30
|
| 3 |
+
pydantic>=2.0
|
| 4 |
pydantic-settings>=2.0
|
| 5 |
asyncpg>=0.29
|
| 6 |
httpx>=0.27
|
| 7 |
curl_cffi>=0.7
|
| 8 |
+
python-multipart>=0.0.7
|