Spaces:
Sleeping
Sleeping
Polish price formatting and add remote validation script
Browse files- README.md +1 -0
- router.py +2 -2
- scripts/deploy.py +1 -1
- scripts/smoke_remote.py +70 -0
README.md
CHANGED
|
@@ -107,6 +107,7 @@ attachments.py PDF / Office / Excel / 图片处理
|
|
| 107 |
requirements.txt Python 依赖(Gradio 由平台管理)
|
| 108 |
packages.txt 系统依赖(LibreOffice、中文字体)
|
| 109 |
scripts/deploy.py 私有 Space 创建、Secret 配置与上传
|
|
|
|
| 110 |
```
|
| 111 |
|
| 112 |
部署后的验收:确认 `RUNNING` 和启动日志;分别选择三个模型发送短消息;上传 PDF、DOCX、PPTX、XLSX 和图片各一份;追问附件内容;测试开启/关闭联网与来源显示;验证停止、新建对话和无效文件提示。仅语法检查不能代替这些线上验证。
|
|
|
|
| 107 |
requirements.txt Python 依赖(Gradio 由平台管理)
|
| 108 |
packages.txt 系统依赖(LibreOffice、中文字体)
|
| 109 |
scripts/deploy.py 私有 Space 创建、Secret 配置与上传
|
| 110 |
+
scripts/smoke_remote.py 远程 Space 端到端验收(会产生少量 API 用量)
|
| 111 |
```
|
| 112 |
|
| 113 |
部署后的验收:确认 `RUNNING` 和启动日志;分别选择三个模型发送短消息;上传 PDF、DOCX、PPTX、XLSX 和图片各一份;追问附件内容;测试开启/关闭联网与来源显示;验证停止、新建对话和无效文件提示。仅语法检查不能代替这些线上验证。
|
router.py
CHANGED
|
@@ -46,8 +46,8 @@ def price_catalog(force: bool = False) -> tuple[list, str]:
|
|
| 46 |
m = data.get(model_id)
|
| 47 |
try:
|
| 48 |
p = m["pricing"]
|
| 49 |
-
inp =
|
| 50 |
-
out =
|
| 51 |
search = f'${Decimal(p["web_search"]):g}/次' if p.get("web_search") else "按搜索引擎计费"
|
| 52 |
source = "目录报价"
|
| 53 |
except (KeyError, TypeError, ArithmeticError):
|
|
|
|
| 46 |
m = data.get(model_id)
|
| 47 |
try:
|
| 48 |
p = m["pricing"]
|
| 49 |
+
inp = format((Decimal(p["prompt"]) * 1_000_000).normalize(), "f")
|
| 50 |
+
out = format((Decimal(p["completion"]) * 1_000_000).normalize(), "f")
|
| 51 |
search = f'${Decimal(p["web_search"]):g}/次' if p.get("web_search") else "按搜索引擎计费"
|
| 52 |
source = "目录报价"
|
| 53 |
except (KeyError, TypeError, ArithmeticError):
|
scripts/deploy.py
CHANGED
|
@@ -8,7 +8,7 @@ import subprocess
|
|
| 8 |
|
| 9 |
ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
FILES = ["README.md", "app.py", "router.py", "attachments.py", "requirements.txt",
|
| 11 |
-
"packages.txt", ".gitignore", ".env.example", "scripts/deploy.py"]
|
| 12 |
|
| 13 |
|
| 14 |
def run(args: list[str], **kwargs) -> subprocess.CompletedProcess:
|
|
|
|
| 8 |
|
| 9 |
ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
FILES = ["README.md", "app.py", "router.py", "attachments.py", "requirements.txt",
|
| 11 |
+
"packages.txt", ".gitignore", ".env.example", "scripts/deploy.py", "scripts/smoke_remote.py"]
|
| 12 |
|
| 13 |
|
| 14 |
def run(args: list[str], **kwargs) -> subprocess.CompletedProcess:
|
scripts/smoke_remote.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small, billed end-to-end checks against an already deployed private Space.
|
| 2 |
+
|
| 3 |
+
Requires gradio_client and huggingface_hub. Fixtures are deliberately synthetic.
|
| 4 |
+
Never reads or prints the OpenRouter API key; the Space uses its Secret.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from gradio_client import Client, handle_file
|
| 12 |
+
from huggingface_hub import get_token
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def answer_text(result: tuple) -> str:
|
| 16 |
+
messages = result[0]
|
| 17 |
+
if not messages or messages[-1]["role"] != "assistant":
|
| 18 |
+
return ""
|
| 19 |
+
content = messages[-1]["content"]
|
| 20 |
+
if isinstance(content, str):
|
| 21 |
+
return content
|
| 22 |
+
return "\n".join(part.get("text", "") for part in content if part.get("type") == "text")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main() -> None:
|
| 26 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 27 |
+
parser.add_argument("repo_id")
|
| 28 |
+
parser.add_argument("--fixtures", type=Path)
|
| 29 |
+
parser.add_argument("--case", choices=["models", "files", "search", "errors", "prices"], required=True)
|
| 30 |
+
args = parser.parse_args()
|
| 31 |
+
client = Client(args.repo_id, token=get_token(), httpx_kwargs={"timeout": 360}, verbose=False)
|
| 32 |
+
|
| 33 |
+
def chat(text: str, files=None, model="anthropic/claude-sonnet-5.5", search=False, max_tokens=1024):
|
| 34 |
+
result = client.predict(text, files or [], model, search, max_tokens, "low", api_name="/chat")
|
| 35 |
+
answer = answer_text(result)
|
| 36 |
+
print(json.dumps({"model": model, "answer": answer[:2000], "status": result[-1]}, ensure_ascii=False), flush=True)
|
| 37 |
+
return answer, result
|
| 38 |
+
|
| 39 |
+
if args.case == "prices":
|
| 40 |
+
result = client.predict("anthropic/claude-sonnet-5.5", api_name="/prices")
|
| 41 |
+
assert all(name in result[-1] for name in ["Opus 5.5", "Sonnet 5.5", "Fable 5.1"])
|
| 42 |
+
print(result[-1], flush=True)
|
| 43 |
+
elif args.case == "models":
|
| 44 |
+
for slug in ["sonnet-5.5", "opus-5.5", "fable-5.1"]:
|
| 45 |
+
answer, result = chat("This is a connectivity test. Reply with exactly CONNECTED.", model="anthropic/claude-" + slug)
|
| 46 |
+
assert "CONNECTED" in answer, result[-1]
|
| 47 |
+
elif args.case == "files":
|
| 48 |
+
if not args.fixtures:
|
| 49 |
+
parser.error("--fixtures is required")
|
| 50 |
+
files = [handle_file(str(args.fixtures / ("sample." + ext))) for ext in ["pdf", "docx", "pptx", "xlsx", "png", "csv"]]
|
| 51 |
+
answer, result = chat("Read all six attachments. Return the PDF_CODE, WORD_CODE, SLIDE_CODE, SHEET_CODE, CSV_CODE values and the image's dominant color. Be brief.", files=files, max_tokens=2048)
|
| 52 |
+
for code in ["ORANGE-42", "MAPLE-17", "OCEAN-29", "CEDAR-63", "BIRCH-36"]:
|
| 53 |
+
assert code in answer, (code, result[-1])
|
| 54 |
+
assert "red" in answer.lower() or "红" in answer
|
| 55 |
+
answer, result = chat("Without uploading again, what was WORD_CODE in the earlier Word attachment? Reply with its value only.")
|
| 56 |
+
assert "MAPLE-17" in answer, result[-1]
|
| 57 |
+
elif args.case == "search":
|
| 58 |
+
answer, result = chat("Use web search now to find the official Hugging Face Spaces overview documentation. State one hosting option and cite its URL. Keep the answer under 100 words.", search=True, max_tokens=2048)
|
| 59 |
+
assert "huggingface.co" in answer, result[-1]
|
| 60 |
+
assert "参考来源" in answer or "联网搜索 1" in result[-1], "Need evidence of tool execution, not just a guessed URL"
|
| 61 |
+
elif args.case == "errors":
|
| 62 |
+
if not args.fixtures:
|
| 63 |
+
parser.error("--fixtures is required")
|
| 64 |
+
answer, result = chat("Read this file", files=[handle_file(str(args.fixtures / "invalid.pdf"))])
|
| 65 |
+
assert "无法解析" in result[-1] and not answer
|
| 66 |
+
print("PASS: " + args.case, flush=True)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
main()
|