File size: 12,250 Bytes
5c9b31b
7a3dae7
dd27c49
76b1985
dd27c49
 
 
2917461
33c192a
2917461
dd27c49
76b1985
 
dd27c49
 
1d3394d
dd27c49
 
76b1985
 
7a3dae7
76b1985
 
7a3dae7
 
76b1985
 
 
7a3dae7
 
dd27c49
 
2917461
dd27c49
 
 
 
76b1985
 
 
 
dd27c49
 
 
76b1985
2917461
 
 
1d3394d
 
 
7a3dae7
 
 
 
 
 
76b1985
7a3dae7
 
 
 
 
76b1985
 
7a3dae7
 
 
 
 
 
 
76b1985
7a3dae7
 
76b1985
7a3dae7
76b1985
7a3dae7
 
 
dd27c49
 
 
 
 
76b1985
7a3dae7
 
76b1985
 
7a3dae7
dd27c49
7a3dae7
76b1985
dd27c49
76b1985
7a3dae7
76b1985
2917461
 
7a3dae7
33c192a
dd27c49
7a3dae7
33c192a
76b1985
 
 
 
 
 
7a3dae7
33c192a
7a3dae7
33c192a
 
7a3dae7
5c9b31b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2917461
 
7a3dae7
 
76b1985
 
 
 
 
 
 
 
 
33c192a
2917461
 
33c192a
 
2917461
33c192a
 
 
76b1985
7a3dae7
76b1985
2917461
 
7a3dae7
2917461
76b1985
 
2917461
 
7a3dae7
76b1985
 
 
 
7a3dae7
2917461
76b1985
33c192a
 
 
 
7a3dae7
 
 
 
 
2917461
 
 
 
7a3dae7
2917461
 
 
7a3dae7
2917461
7a3dae7
 
 
76b1985
 
 
 
 
 
 
 
 
 
 
 
 
 
7a3dae7
 
 
 
76b1985
7a3dae7
 
 
 
 
76b1985
7a3dae7
76b1985
33c192a
 
 
 
7a3dae7
 
76b1985
 
 
 
 
7a3dae7
 
76b1985
 
 
 
 
 
7a3dae7
 
76b1985
 
7a3dae7
 
33c192a
 
 
 
7a3dae7
76b1985
 
 
 
 
 
 
7a3dae7
 
 
76b1985
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import json
import os, json, traceback
from pathlib import Path
from fastapi import FastAPI, Request, BackgroundTasks
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import config as cfg
from agent_system import orchestrator
from sandbox import pip_install

app = FastAPI(title="PraisonChat", version="6.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

STATIC_DIR = Path(__file__).parent / "static"
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")


# ── Always return JSON errors, never HTML ─────────────────────
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def json_http_handler(request, exc):
    return JSONResponse(status_code=exc.status_code, content={"ok":False,"detail":str(exc.detail)})

@app.exception_handler(Exception)
async def json_generic_handler(request, exc):
    print(f"[SERVER] {exc}\n{traceback.format_exc()}")
    return JSONResponse(status_code=500, content={"ok":False,"detail":str(exc)})


@app.get("/", response_class=HTMLResponse)
async def root():
    return HTMLResponse((STATIC_DIR / "index.html").read_text(encoding="utf-8"))


@app.get("/api/health")
def health():
    return {"ok":True,"version":"6.0.0",
            "longcat_key":bool(cfg.get_longcat_key()),
            "telegram":bool(cfg.get_telegram_token())}


@app.get("/api/models")
def models():
    return {"models":[
        {"id":"LongCat-Flash-Lite",         "name":"LongCat Flash Lite",     "context":"320K","speed":"⚑ Fastest","quota":"50M/day"},
        {"id":"LongCat-Flash-Chat",          "name":"LongCat Flash Chat",     "context":"256K","speed":"πŸš€ Fast",   "quota":"500K/day"},
        {"id":"LongCat-Flash-Thinking-2601", "name":"LongCat Flash Thinking", "context":"256K","speed":"🧠 Deep",   "quota":"500K/day"},
    ]}


@app.get("/api/memory")
def get_memory():
    from memory import get_all_for_api
    return get_all_for_api()

@app.delete("/api/memory/{key}")
def del_memory(key: str):
    from memory import MEM_DIR
    safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in key)
    path = MEM_DIR / f"{safe}.md"
    if path.exists():
        path.unlink()
        return {"ok":True}
    return {"ok":False,"detail":"Not found"}

@app.get("/api/skills")
def get_skills():
    from memory import list_skills, load_skill
    skills = list_skills()
    for s in skills:
        s["code"] = load_skill(s["name"])[:800]
    return {"skills":skills}

@app.delete("/api/skills/{name}")
def del_skill(name: str):
    from memory import delete_skill
    return {"ok": delete_skill(name)}


# ── Chat ───────────────────────────────────────────────────────
@app.post("/api/chat")
async def chat(request: Request):
    try:
        body = await request.json()
    except Exception:
        return JSONResponse(status_code=400, content={"ok":False,"detail":"Invalid JSON body"})

    messages = body.get("messages", [])
    api_key  = (body.get("api_key") or "").strip() or cfg.get_longcat_key()
    model    = body.get("model", "LongCat-Flash-Lite")

    if not api_key:
        return JSONResponse(status_code=400,
            content={"ok":False,"detail":"LongCat API key required. Go to Settings (βš™οΈ) and paste your key."})
    if not messages:
        return JSONResponse(status_code=400, content={"ok":False,"detail":"No messages"})

    # Save for Telegram
    if api_key != cfg.get_longcat_key(): cfg.set_longcat_key(api_key)
    if model   != cfg.get_model():       cfg.set_model(model)

    user_message = messages[-1].get("content","")
    history      = messages[:-1]

    async def stream():
        try:
            async for chunk in orchestrator.stream_response(user_message, history, api_key, model):
                yield f"data: {chunk}\n\n"
        except Exception as e:
            err = json.dumps({"type":"error","message":str(e)})
            yield f"data: {err}\n\n"

    return StreamingResponse(stream(), media_type="text/event-stream",
        headers={"X-Accel-Buffering":"no","Cache-Control":"no-cache","Connection":"keep-alive"})


# ── Package installer ──────────────────────────────────────────

# ── Cross-platform: Web SSE push from Telegram ────────────────
import asyncio as _asyncio
_sse_queues: dict = {}  # session_id -> asyncio.Queue

@app.get("/api/sse/{session_id}")
async def sse_endpoint(session_id: str, request: Request):
    """SSE stream β€” pushes messages from Telegram to Web UI."""
    queue = _asyncio.Queue()
    _sse_queues[session_id] = queue

    from agent_system import orchestrator
    async def tg_notify(role, content):
        await queue.put(json.dumps({"type":"tg_message","role":role,"content":content}))
    orchestrator.register_tg_notify(session_id, tg_notify)

    async def event_stream():
        try:
            yield "data: " + json.dumps({"type":"connected","session":session_id}) + "\n\n"
            while True:
                if await request.is_disconnected():
                    break
                try:
                    msg = await _asyncio.wait_for(queue.get(), timeout=25)
                    yield f"data: {msg}\n\n"
                except _asyncio.TimeoutError:
                    yield "data: " + json.dumps({"type":"ping"}) + "\n\n"
        finally:
            _sse_queues.pop(session_id, None)
            orchestrator.unregister_tg_notify(session_id)

    return StreamingResponse(event_stream(), media_type="text/event-stream",
        headers={"X-Accel-Buffering":"no","Cache-Control":"no-cache"})


@app.post("/api/send-to-telegram")
async def send_to_telegram(request: Request):
    """Send a web UI message to linked Telegram chat."""
    try:
        body = await request.json()
        chat_id  = body.get("chat_id")
        message  = body.get("message","")
        api_key  = (body.get("api_key") or "").strip() or cfg.get_longcat_key()
        model    = body.get("model","LongCat-Flash-Lite")
        if not chat_id:
            return JSONResponse({"ok":False,"detail":"No chat_id"})
        from telegram_bot import handle_update
        fake_update = {"message":{"chat":{"id":chat_id},"from":{"first_name":"WebUI"},"text":message}}
        import asyncio as _aio
        _aio.create_task(handle_update(fake_update, api_key, model))
        return {"ok":True}
    except Exception as e:
        return JSONResponse(status_code=500, content={"ok":False,"detail":str(e)})

@app.post("/api/install-package")
async def install_package(request: Request):
    try:
        body = await request.json()
        packages = body.get("packages", [])
        if not packages:
            return JSONResponse(status_code=400, content={"ok":False,"detail":"No packages"})
        import asyncio
        loop = asyncio.get_event_loop()
        ok, msg = await loop.run_in_executor(None, pip_install, packages)
        return {"ok":ok,"message":msg}
    except Exception as e:
        return JSONResponse(status_code=500, content={"ok":False,"detail":str(e)})


# ── Telegram ───────────────────────────────────────────────────
@app.post("/telegram/webhook")
async def telegram_webhook(request: Request, background_tasks: BackgroundTasks):
    from telegram_bot import handle_update
    try:
        update = await request.json()
    except Exception:
        return JSONResponse({"ok":True})

    print(f"[WEBHOOK] {str(update)[:120]}")
    api_key = cfg.get_longcat_key()
    model   = cfg.get_model()

    if not api_key:
        msg     = update.get("message",{})
        chat_id = msg.get("chat",{}).get("id")
        if chat_id:
            from telegram_bot import send_message
            await send_message(chat_id,
                "⚠️ No LongCat API key saved!\n\n"
                "Open the web app β†’ Settings (βš™οΈ) β†’ paste your LongCat key β†’ Save.\n"
                "Or set LONGCAT_API_KEY as a HuggingFace Space Secret.")
        return JSONResponse({"ok":True})

    background_tasks.add_task(handle_update, update, api_key, model)
    return JSONResponse({"ok":True})


@app.post("/api/telegram/setup")
async def telegram_setup(request: Request):
    try:
        body = await request.json()
    except Exception:
        return JSONResponse(status_code=400, content={"ok":False,"detail":"Invalid JSON"})

    token    = body.get("token","").strip()
    base_url = body.get("base_url","").strip()
    api_key  = body.get("api_key","").strip()
    model    = body.get("model","").strip()

    if token:   cfg.set_telegram_token(token)
    if api_key: cfg.set_longcat_key(api_key)
    if model:   cfg.set_model(model)

    if not cfg.get_telegram_token():
        return JSONResponse(status_code=400,
            content={"ok":False,"detail":"No Telegram bot token provided"})

    # Check DNS first
    from telegram_bot import check_telegram_reachable, get_bot_info, set_webhook
    import asyncio
    loop = asyncio.get_event_loop()
    reachable, dns_msg = await loop.run_in_executor(None, check_telegram_reachable)
    if not reachable:
        return JSONResponse(status_code=503, content={
            "ok": False,
            "detail": f"Cannot reach Telegram API from this server. {dns_msg}\n\n"
                      f"Solution: Set TELEGRAM_BOT_TOKEN as a HuggingFace Space Secret "
                      f"(Settings β†’ Variables and Secrets). The bot will still receive "
                      f"messages via webhook even if this setup fails."
        })

    try:
        bot = await get_bot_info()
        if not bot.get("ok"):
            return JSONResponse(status_code=400,
                content={"ok":False,"detail":f"Invalid bot token: {bot.get('description','check your token')}"})

        webhook_result = {}
        if base_url:
            webhook_result = await set_webhook(base_url)

        return JSONResponse({"ok":True,"bot":bot.get("result",{}),"webhook":webhook_result})
    except Exception as e:
        return JSONResponse(status_code=500, content={"ok":False,"detail":str(e)})


@app.get("/api/telegram/status")
async def telegram_status():
    try:
        token = cfg.get_telegram_token()
        # Also check env var
        env_token = os.environ.get("TELEGRAM_BOT_TOKEN","")
        if env_token and not token:
            cfg.set_telegram_token(env_token)
            token = env_token
        if not token:
            return {"connected":False,"message":"No bot token set"}
        from telegram_bot import check_telegram_reachable, get_bot_info, get_webhook_info
        import asyncio
        loop = asyncio.get_event_loop()
        reachable, _ = await loop.run_in_executor(None, check_telegram_reachable)
        if not reachable:
            return {"connected":False,"message":"Token set but api.telegram.org unreachable from server. Use HF Space Secret TELEGRAM_BOT_TOKEN.","token_set":True}
        bot  = await get_bot_info()
        hook = await get_webhook_info()
        return {"connected":bot.get("ok",False),"bot":bot.get("result",{}),
                "webhook":hook.get("result",{}),"longcat_key_set":bool(cfg.get_longcat_key())}
    except Exception as e:
        return {"connected":False,"message":str(e)}


@app.delete("/api/telegram/disconnect")
async def telegram_disconnect():
    try:
        from telegram_bot import delete_webhook, check_telegram_reachable
        import asyncio
        loop = asyncio.get_event_loop()
        reachable, _ = await loop.run_in_executor(None, check_telegram_reachable)
        result = {}
        if reachable:
            result = await delete_webhook()
        cfg.set("telegram_token","")
        return {"ok":True,"result":result}
    except Exception as e:
        cfg.set("telegram_token","")
        return {"ok":True}