Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -1,19 +1,36 @@
|
|
| 1 |
import html
|
|
|
|
|
|
|
| 2 |
import time
|
|
|
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
import spaces
|
| 5 |
import torch
|
| 6 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from pasita import infer_kind, convert_to_markdown
|
| 10 |
|
| 11 |
MODEL_ID = "OpceanAI/PASITA"
|
| 12 |
MODEL_CARD = "https://huggingface.co/OpceanAI/PASITA"
|
|
|
|
| 13 |
|
| 14 |
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 15 |
tokenizer.padding_side = "left"
|
| 16 |
-
|
|
|
|
| 17 |
model.eval()
|
| 18 |
|
| 19 |
EMPTY_MD = "_The converted Markdown appears here. Paste a document on the left and press Convert._"
|
|
@@ -48,6 +65,25 @@ def _estimate_duration(text, num_candidates=3, structured_fallback=True, *args,
|
|
| 48 |
return min(30, max(12, 8 + len(text or "") // 200 + 4 * (int(num_candidates) - 1)))
|
| 49 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
@spaces.GPU(duration=_estimate_duration)
|
| 52 |
def convert(text: str, num_candidates: int = 3, structured_fallback: bool = True) -> tuple[str, str, str]:
|
| 53 |
"""Convert a plain-text document into faithful GitHub-Flavored Markdown.
|
|
@@ -68,27 +104,14 @@ def convert(text: str, num_candidates: int = 3, structured_fallback: bool = True
|
|
| 68 |
if not text:
|
| 69 |
return EMPTY_MD, "", _status("READY / PASTE A DOCUMENT TO CONVERT")
|
| 70 |
|
| 71 |
-
started = time.perf_counter()
|
| 72 |
try:
|
| 73 |
-
result =
|
| 74 |
-
model,
|
| 75 |
-
tokenizer,
|
| 76 |
-
text,
|
| 77 |
-
num_candidates=max(1, min(3, int(num_candidates))),
|
| 78 |
-
do_sample=True,
|
| 79 |
-
temperature=0.3,
|
| 80 |
-
top_p=0.95,
|
| 81 |
-
seed=0,
|
| 82 |
-
use_cache=True,
|
| 83 |
-
structured_fallback=bool(structured_fallback),
|
| 84 |
-
skip_model_for_structured=True,
|
| 85 |
-
)
|
| 86 |
except Exception as exc:
|
| 87 |
message = html.escape(f"{type(exc).__name__}: {exc}")
|
| 88 |
return (
|
| 89 |
f"> Conversion failed.\n>\n> `{message}`",
|
| 90 |
"",
|
| 91 |
-
_status(f"ERROR / {type(exc).__name__}", "error"
|
| 92 |
)
|
| 93 |
|
| 94 |
metrics = result["metrics"]
|
|
@@ -100,15 +123,243 @@ def convert(text: str, num_candidates: int = 3, structured_fallback: bool = True
|
|
| 100 |
"passthrough": "SOURCE PASSTHROUGH",
|
| 101 |
}
|
| 102 |
source_label = routes.get(result.get("route"), "PASITA + RERANK")
|
| 103 |
-
taken = result["seconds"] if result["seconds"] else
|
| 104 |
detail = f"{source_label} / {numbers} / {overlap} / {taken:.1f}S"
|
| 105 |
return result["markdown"], result["markdown"], _status("DONE", "ready", detail)
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def reset() -> tuple[str, str, str, str]:
|
| 109 |
return EMPTY_MD, "", _status("READY"), _counter("")
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
BUSY_JS = """(text, num_candidates, structured_fallback, md, source, status) => {
|
| 113 |
const n = Number(num_candidates) || 1;
|
| 114 |
const label = n > 1 ? 'PASITA ' + n + ' SAMPLES / RERANK' : 'PASITA SAMPLE / RERANK';
|
|
@@ -469,4 +720,4 @@ with gr.Blocks(title="PASITA: plain text to Markdown") as demo:
|
|
| 469 |
clear.click(reset, inputs=None, outputs=[md_out, source, status, label_in], api_visibility="undocumented")
|
| 470 |
|
| 471 |
demo.queue()
|
| 472 |
-
demo.launch(theme=theme, css=CSS, mcp_server=True)
|
|
|
|
| 1 |
import html
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
import time
|
| 5 |
+
import uuid
|
| 6 |
|
| 7 |
+
os.environ.setdefault("GRADIO_SSR_MODE", "false")
|
| 8 |
+
|
| 9 |
+
import fastapi
|
| 10 |
import spaces
|
| 11 |
import torch
|
| 12 |
import gradio as gr
|
| 13 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 14 |
+
from gradio import Server
|
| 15 |
+
from gradio.context import LocalContext
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 18 |
|
| 19 |
+
try:
|
| 20 |
+
from gradio.route_utils import Request as GradioRequest
|
| 21 |
+
except ImportError:
|
| 22 |
+
GradioRequest = None
|
| 23 |
+
|
| 24 |
from pasita import infer_kind, convert_to_markdown
|
| 25 |
|
| 26 |
MODEL_ID = "OpceanAI/PASITA"
|
| 27 |
MODEL_CARD = "https://huggingface.co/OpceanAI/PASITA"
|
| 28 |
+
MAX_INPUT_CHARS = 60_000
|
| 29 |
|
| 30 |
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 31 |
tokenizer.padding_side = "left"
|
| 32 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 33 |
+
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype="auto").to(DEVICE)
|
| 34 |
model.eval()
|
| 35 |
|
| 36 |
EMPTY_MD = "_The converted Markdown appears here. Paste a document on the left and press Convert._"
|
|
|
|
| 65 |
return min(30, max(12, 8 + len(text or "") // 200 + 4 * (int(num_candidates) - 1)))
|
| 66 |
|
| 67 |
|
| 68 |
+
def _run_harness(text: str, num_candidates: int, structured_fallback: bool) -> dict:
|
| 69 |
+
started = time.perf_counter()
|
| 70 |
+
result = convert_to_markdown(
|
| 71 |
+
model,
|
| 72 |
+
tokenizer,
|
| 73 |
+
text,
|
| 74 |
+
num_candidates=max(1, min(3, int(num_candidates))),
|
| 75 |
+
do_sample=True,
|
| 76 |
+
temperature=0.3,
|
| 77 |
+
top_p=0.95,
|
| 78 |
+
seed=0,
|
| 79 |
+
use_cache=True,
|
| 80 |
+
structured_fallback=bool(structured_fallback),
|
| 81 |
+
skip_model_for_structured=True,
|
| 82 |
+
)
|
| 83 |
+
result["elapsed"] = time.perf_counter() - started
|
| 84 |
+
return result
|
| 85 |
+
|
| 86 |
+
|
| 87 |
@spaces.GPU(duration=_estimate_duration)
|
| 88 |
def convert(text: str, num_candidates: int = 3, structured_fallback: bool = True) -> tuple[str, str, str]:
|
| 89 |
"""Convert a plain-text document into faithful GitHub-Flavored Markdown.
|
|
|
|
| 104 |
if not text:
|
| 105 |
return EMPTY_MD, "", _status("READY / PASTE A DOCUMENT TO CONVERT")
|
| 106 |
|
|
|
|
| 107 |
try:
|
| 108 |
+
result = _run_harness(text, num_candidates, structured_fallback)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
except Exception as exc:
|
| 110 |
message = html.escape(f"{type(exc).__name__}: {exc}")
|
| 111 |
return (
|
| 112 |
f"> Conversion failed.\n>\n> `{message}`",
|
| 113 |
"",
|
| 114 |
+
_status(f"ERROR / {type(exc).__name__}", "error"),
|
| 115 |
)
|
| 116 |
|
| 117 |
metrics = result["metrics"]
|
|
|
|
| 123 |
"passthrough": "SOURCE PASSTHROUGH",
|
| 124 |
}
|
| 125 |
source_label = routes.get(result.get("route"), "PASITA + RERANK")
|
| 126 |
+
taken = result["seconds"] if result["seconds"] else result["elapsed"]
|
| 127 |
detail = f"{source_label} / {numbers} / {overlap} / {taken:.1f}S"
|
| 128 |
return result["markdown"], result["markdown"], _status("DONE", "ready", detail)
|
| 129 |
|
| 130 |
|
| 131 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 132 |
+
def gpu_convert(text: str, num_candidates: int = 3, structured_fallback: bool = True) -> dict:
|
| 133 |
+
"""Convert a document and return the full conversion result.
|
| 134 |
+
|
| 135 |
+
Same pipeline as the UI conversion. Used by the /v1 OpenAI-style API routes.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
text: The document to convert.
|
| 139 |
+
num_candidates: How many PASITA samples to generate and rerank, from 1 to 3.
|
| 140 |
+
structured_fallback: Use the deterministic converter for HTML and delimited data.
|
| 141 |
+
Returns:
|
| 142 |
+
The full result dict: markdown, route, metrics, timing and candidate info.
|
| 143 |
+
"""
|
| 144 |
+
return _run_harness(text, num_candidates, structured_fallback)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
COUNTER_JS = """(text) => {
|
| 148 |
+
const t = (text || '').trim();
|
| 149 |
+
const words = t ? t.split(/\\s+/).length : 0;
|
| 150 |
+
const chars = (text || '').length;
|
| 151 |
+
return '<span class="p-label">PLAIN TEXT</span><span class="p-count">' + chars.toLocaleString() + ' CHARS / ' + words.toLocaleString() + ' WORDS</span>';
|
| 152 |
+
}"""
|
| 153 |
+
|
| 154 |
+
API_DESCRIPTION = """
|
| 155 |
+
OpenAI-style REST API for [PASITA](https://huggingface.co/OpceanAI/PASITA), an 88M
|
| 156 |
+
from-scratch model that turns plain text into faithful Markdown. No API key is required.
|
| 157 |
+
Works with the OpenAI SDK by pointing `base_url` at `/v1`. The Gradio API and the MCP
|
| 158 |
+
server stay available at `?view=api`.
|
| 159 |
+
""".strip()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _api_error(message: str, error_type: str = "invalid_request_error", status: int = 400, code: str | None = None) -> JSONResponse:
|
| 163 |
+
return JSONResponse(
|
| 164 |
+
status_code=status,
|
| 165 |
+
content={"error": {"message": message, "type": error_type, "param": None, "code": code}},
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _with_request_context(request: fastapi.Request, text: str, candidates: int, fallback: bool) -> dict:
|
| 170 |
+
if GradioRequest is not None:
|
| 171 |
+
gr_request = GradioRequest(request=request, session_hash="api")
|
| 172 |
+
token = LocalContext.request.set(gr_request)
|
| 173 |
+
try:
|
| 174 |
+
return gpu_convert(text, candidates, fallback)
|
| 175 |
+
finally:
|
| 176 |
+
LocalContext.request.reset(token)
|
| 177 |
+
return gpu_convert(text, candidates, fallback)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _token_counts(text: str, markdown: str) -> tuple[int, int]:
|
| 181 |
+
prompt_tokens = len(tokenizer(text, add_special_tokens=True)["input_ids"])
|
| 182 |
+
completion_tokens = len(tokenizer(markdown, add_special_tokens=False)["input_ids"])
|
| 183 |
+
return prompt_tokens, completion_tokens
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class ChatMessage(BaseModel):
|
| 187 |
+
role: str = "user"
|
| 188 |
+
content: str | list | None = None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class ChatCompletionRequest(BaseModel):
|
| 192 |
+
model: str | None = "pasita-v1"
|
| 193 |
+
messages: list[ChatMessage]
|
| 194 |
+
stream: bool = False
|
| 195 |
+
candidates: int = 3
|
| 196 |
+
structured_fallback: bool = True
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class ConvertRequest(BaseModel):
|
| 200 |
+
text: str
|
| 201 |
+
candidates: int = 3
|
| 202 |
+
structured_fallback: bool = True
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
server = Server(title="PASITA API", version="1.0.0", description=API_DESCRIPTION)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@server.exception_handler(fastapi.exceptions.RequestValidationError)
|
| 209 |
+
async def validation_error_handler(request: fastapi.Request, exc: fastapi.exceptions.RequestValidationError):
|
| 210 |
+
issues = "; ".join(f"{'.'.join(str(l) for l in e.get('loc', []))}: {e.get('msg', '')}" for e in exc.errors()[:3])
|
| 211 |
+
return _api_error(f"Invalid request body: {issues}", status=400)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@server.get("/v1/models")
|
| 215 |
+
def list_models() -> dict:
|
| 216 |
+
return {
|
| 217 |
+
"object": "list",
|
| 218 |
+
"data": [{"id": "pasita-v1", "object": "model", "created": 1760000000, "owned_by": "OpceanAI"}],
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
@server.get("/v1/health")
|
| 223 |
+
def health() -> dict:
|
| 224 |
+
return {"status": "ok", "model": "pasita-v1"}
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _validate(body: ChatCompletionRequest | ConvertRequest, text: str | None) -> JSONResponse | None:
|
| 228 |
+
if text is None or not text.strip():
|
| 229 |
+
return _api_error("No input text was provided.", code="empty_input")
|
| 230 |
+
if len(text) > MAX_INPUT_CHARS:
|
| 231 |
+
return _api_error(
|
| 232 |
+
f"The document is too long ({len(text):,} chars). The limit is {MAX_INPUT_CHARS:,} characters.",
|
| 233 |
+
code="input_too_long",
|
| 234 |
+
)
|
| 235 |
+
candidates = getattr(body, "candidates", 3)
|
| 236 |
+
if not 1 <= int(candidates) <= 3:
|
| 237 |
+
return _api_error("candidates must be between 1 and 3.", code="invalid_candidates")
|
| 238 |
+
return None
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _result_payload(result: dict, text: str, started: float) -> dict:
|
| 242 |
+
markdown = result["markdown"]
|
| 243 |
+
prompt_tokens, completion_tokens = _token_counts(text, markdown)
|
| 244 |
+
return {
|
| 245 |
+
"markdown": markdown,
|
| 246 |
+
"route": result.get("route", "model"),
|
| 247 |
+
"metrics": result["metrics"],
|
| 248 |
+
"candidates": len(result.get("candidates", [])),
|
| 249 |
+
"seconds": round(result["seconds"] or result["elapsed"], 2),
|
| 250 |
+
"usage": {
|
| 251 |
+
"prompt_tokens": prompt_tokens,
|
| 252 |
+
"completion_tokens": completion_tokens,
|
| 253 |
+
"total_tokens": prompt_tokens + completion_tokens,
|
| 254 |
+
},
|
| 255 |
+
"elapsed": round(time.perf_counter() - started, 2),
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
@server.post("/v1/convert")
|
| 260 |
+
def v1_convert(body: ConvertRequest, request: fastapi.Request) -> dict | JSONResponse:
|
| 261 |
+
error = _validate(body, body.text)
|
| 262 |
+
if error is not None:
|
| 263 |
+
return error
|
| 264 |
+
started = time.perf_counter()
|
| 265 |
+
try:
|
| 266 |
+
result = _with_request_context(request, body.text.strip(), int(body.candidates), bool(body.structured_fallback))
|
| 267 |
+
except Exception as exc:
|
| 268 |
+
return _api_error(f"{type(exc).__name__}: {exc}", "server_error", status=500)
|
| 269 |
+
payload = _result_payload(result, body.text.strip(), started)
|
| 270 |
+
payload.update({"id": f"conv-{uuid.uuid4().hex[:24]}", "object": "pasita.conversion", "created": int(time.time()), "model": "pasita-v1"})
|
| 271 |
+
return payload
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def _extract_user_text(messages: list[ChatMessage]) -> str | None:
|
| 275 |
+
for message in reversed(messages):
|
| 276 |
+
if message.role != "user":
|
| 277 |
+
continue
|
| 278 |
+
content = message.content
|
| 279 |
+
if isinstance(content, str):
|
| 280 |
+
return content
|
| 281 |
+
if isinstance(content, list):
|
| 282 |
+
parts = []
|
| 283 |
+
for part in content:
|
| 284 |
+
if isinstance(part, dict) and part.get("type") == "text":
|
| 285 |
+
parts.append(str(part.get("text", "")))
|
| 286 |
+
elif isinstance(part, str):
|
| 287 |
+
parts.append(part)
|
| 288 |
+
return "\n".join(p for p in parts if p)
|
| 289 |
+
return None
|
| 290 |
+
return None
|
| 291 |
+
|
| 292 |
+
|
| 293 |
def reset() -> tuple[str, str, str, str]:
|
| 294 |
return EMPTY_MD, "", _status("READY"), _counter("")
|
| 295 |
|
| 296 |
|
| 297 |
+
@server.post("/v1/chat/completions")
|
| 298 |
+
def chat_completions(body: ChatCompletionRequest, request: fastapi.Request) -> dict | JSONResponse | StreamingResponse:
|
| 299 |
+
text = _extract_user_text(body.messages)
|
| 300 |
+
error = _validate(body, text)
|
| 301 |
+
if error is not None:
|
| 302 |
+
return error
|
| 303 |
+
|
| 304 |
+
started = time.perf_counter()
|
| 305 |
+
try:
|
| 306 |
+
result = _with_request_context(request, text.strip(), int(body.candidates), bool(body.structured_fallback))
|
| 307 |
+
except Exception as exc:
|
| 308 |
+
return _api_error(f"{type(exc).__name__}: {exc}", "server_error", status=500)
|
| 309 |
+
|
| 310 |
+
payload = _result_payload(result, text.strip(), started)
|
| 311 |
+
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
| 312 |
+
created = int(time.time())
|
| 313 |
+
markdown = payload["markdown"]
|
| 314 |
+
|
| 315 |
+
if body.stream:
|
| 316 |
+
def sse():
|
| 317 |
+
def chunk(delta, finish=None, usage=None):
|
| 318 |
+
data = {
|
| 319 |
+
"id": completion_id,
|
| 320 |
+
"object": "chat.completion.chunk",
|
| 321 |
+
"created": created,
|
| 322 |
+
"model": "pasita-v1",
|
| 323 |
+
"choices": [{"index": 0, "delta": delta, "finish_reason": finish}],
|
| 324 |
+
}
|
| 325 |
+
if usage is not None:
|
| 326 |
+
data["usage"] = usage
|
| 327 |
+
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
| 328 |
+
|
| 329 |
+
yield chunk({"role": "assistant", "content": ""})
|
| 330 |
+
yield chunk({"content": markdown})
|
| 331 |
+
yield chunk({}, finish="stop", usage=payload["usage"])
|
| 332 |
+
yield "data: [DONE]\n\n"
|
| 333 |
+
|
| 334 |
+
return StreamingResponse(
|
| 335 |
+
sse(),
|
| 336 |
+
media_type="text/event-stream",
|
| 337 |
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
return {
|
| 341 |
+
"id": completion_id,
|
| 342 |
+
"object": "chat.completion",
|
| 343 |
+
"created": created,
|
| 344 |
+
"model": "pasita-v1",
|
| 345 |
+
"choices": [
|
| 346 |
+
{
|
| 347 |
+
"index": 0,
|
| 348 |
+
"message": {"role": "assistant", "content": markdown},
|
| 349 |
+
"logprobs": None,
|
| 350 |
+
"finish_reason": "stop",
|
| 351 |
+
}
|
| 352 |
+
],
|
| 353 |
+
"usage": payload["usage"],
|
| 354 |
+
"pasita": {
|
| 355 |
+
"route": payload["route"],
|
| 356 |
+
"metrics": payload["metrics"],
|
| 357 |
+
"candidates": payload["candidates"],
|
| 358 |
+
"seconds": payload["seconds"],
|
| 359 |
+
},
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
|
| 363 |
BUSY_JS = """(text, num_candidates, structured_fallback, md, source, status) => {
|
| 364 |
const n = Number(num_candidates) || 1;
|
| 365 |
const label = n > 1 ? 'PASITA ' + n + ' SAMPLES / RERANK' : 'PASITA SAMPLE / RERANK';
|
|
|
|
| 720 |
clear.click(reset, inputs=None, outputs=[md_out, source, status, label_in], api_visibility="undocumented")
|
| 721 |
|
| 722 |
demo.queue()
|
| 723 |
+
demo.launch(_app=server, theme=theme, css=CSS, mcp_server=True)
|