Add Dream QA agent readiness API
#28
by ADJCJH - opened
- AGENTS.md +0 -119
- agents.md +72 -0
- dream_customs/ui/app.py +90 -2
- scripts/evaluate_agent_readiness.py +183 -0
- tests/test_agent_readiness.py +56 -0
- tests/test_ui_actions.py +2 -0
AGENTS.md
DELETED
|
@@ -1,119 +0,0 @@
|
|
| 1 |
-
# AGENTS.md - Dream QA / 梦境问答台
|
| 2 |
-
|
| 3 |
-
## 语言规则
|
| 4 |
-
|
| 5 |
-
- 默认使用中文回复用户。
|
| 6 |
-
- 面向评审/README/Space 展示的文字可以中英双语,但不要牺牲中文表达的清晰度。
|
| 7 |
-
|
| 8 |
-
## 项目定位
|
| 9 |
-
|
| 10 |
-
- 当前项目是 Build Small Hackathon 的 `Dream QA / 梦境问答台`,代码仓库和 Space 仍沿用 `Dream Customs` 名称以保持部署连续性。
|
| 11 |
-
- 核心定位:做一个循序渐进的解梦 Gradio app,帮助用户一步步说清梦境、表达疑惑、回答追问,最后得到一个温和的 `今日小 Tips`。
|
| 12 |
-
- `今日小 Tips` 可以是一个认真建议、一个用户未尝试过的小事,或一句关心用户的短句;它必须引用梦里的具体细节,不能变成泛泛鸡汤。
|
| 13 |
-
- 这不是心理诊断、医疗建议、治疗工具、占卜工具或宿命论解释器。
|
| 14 |
-
|
| 15 |
-
## 竞赛约束
|
| 16 |
-
|
| 17 |
-
- 目标交付:Gradio app on Hugging Face Space。
|
| 18 |
-
- 模型总参数必须 <= 32B。
|
| 19 |
-
- 优先叙事赛道:An Adventure in Thousand Token Wood。
|
| 20 |
-
- 次级叙事:Backyard AI,因为项目来自真实睡眠/做梦困扰。
|
| 21 |
-
- demo 必须能在短视频中快速展示:输入梦境,回答或跳过追问,获得今日小 Tips。
|
| 22 |
-
|
| 23 |
-
## 模型规则
|
| 24 |
-
|
| 25 |
-
- 首选模型组合:
|
| 26 |
-
- `openbmb/MiniCPM-V-4.6`:图片、草图、便签、截图理解。
|
| 27 |
-
- `openbmb/MiniCPM5-1B`:文本推理、追问生成、梦境解读草稿、结构化今日 Tips。
|
| 28 |
-
- 不要在 MiniCPM 路线未验证前扩展到任意其他小模型。
|
| 29 |
-
- 语音输入需要 ASR 适配器;ASR 只做转写,不承担梦境理解。
|
| 30 |
-
- 首版不要实现语音输出,避免扩大部署风险。
|
| 31 |
-
|
| 32 |
-
## 产品边界
|
| 33 |
-
|
| 34 |
-
- 必须支持文字输入。
|
| 35 |
-
- 应支持图片输入,用 MiniCPM-V-4.6 提取视觉线索。
|
| 36 |
-
- 应支持语音输入,通过小 ASR 适配器转成文本。
|
| 37 |
-
- 所有输入统一进入同一个梦境 intake,不要做成三套割裂产品。
|
| 38 |
-
- 用户流程必须是渐进式的:
|
| 39 |
-
1. 记录梦境。
|
| 40 |
-
2. 明确用户最想理解的问题。
|
| 41 |
-
3. 生成 1-3 个温和追问。
|
| 42 |
-
4. 用户回答、跳过或请求换个角度。
|
| 43 |
-
5. 生成梦境解读草稿。
|
| 44 |
-
6. 输出一个今日小 Tips。
|
| 45 |
-
- 输出必须包含:
|
| 46 |
-
- `dream_summary` / 梦境摘要
|
| 47 |
-
- `main_question` / 用户最想理解的问题
|
| 48 |
-
- `dream_anchors` / 梦境具体线索
|
| 49 |
-
- `followup_questions` / 追问
|
| 50 |
-
- `user_answers` / 用户回答或跳过记录
|
| 51 |
-
- `interpretation` / 非确定性的解读草稿
|
| 52 |
-
- `today_tip` / 今日小 Tips
|
| 53 |
-
- `tiny_action` / 可选的小行动
|
| 54 |
-
- `caring_note` / 可选的关心语句
|
| 55 |
-
- `safety_note` / 必要时的安全提示
|
| 56 |
-
|
| 57 |
-
## 安全规则
|
| 58 |
-
|
| 59 |
-
- 不做心理疾病诊断。
|
| 60 |
-
- 不声称梦境有唯一含义。
|
| 61 |
-
- 不输出恐吓性、宿命论、神秘绝对化内容。
|
| 62 |
-
- 不把梦解释成“预兆”“证明”“创伤证据”或医学结论。
|
| 63 |
-
- 普通输出使用“也许”“可以把它当作”“今天先试试”这类非确定措辞。
|
| 64 |
-
- 如果用户表达自伤、伤人、长期严重失眠、无法正常生活或极强痛苦,必须显示寻求可信任的人或专业支持的提示。
|
| 65 |
-
|
| 66 |
-
## 文档优先级
|
| 67 |
-
|
| 68 |
-
实现前先阅读:
|
| 69 |
-
|
| 70 |
-
1. `docs/handoff.md`
|
| 71 |
-
2. `docs/spec.md`
|
| 72 |
-
3. `docs/prd.md`
|
| 73 |
-
4. `PRODUCT.md`
|
| 74 |
-
5. `DESIGN.md`
|
| 75 |
-
6. `docs/superpowers/plans/2026-06-08-dream-qa-refactor.md`
|
| 76 |
-
7. `docs/superpowers/plans/2026-06-08-dream-qa-refactor-doc-plan.md`
|
| 77 |
-
|
| 78 |
-
旧的 pact/customs 计划、smoke 和概念页是历史记录。除非用户明确要求复盘历史,否则不要再把 permit、contraband、sealed pact 当作新实现目标。
|
| 79 |
-
|
| 80 |
-
## 开发流程
|
| 81 |
-
|
| 82 |
-
- 修改或实验前先确认工作目录是 `/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon`。
|
| 83 |
-
- 当前目录可能位于父级 `vlarepo` git root 下;不要误推到 VLA remote。
|
| 84 |
-
- 如果需要正式提交,先确认是否已初始化/切换到 Dream Customs 专用仓库。
|
| 85 |
-
- 在仓库归属未确认前,可以本地写文件,但不要强行 `git add` / `git commit` / `git push` 到父级 VLA 仓库。
|
| 86 |
-
- 如果建立新功能分支,使用 `feature/xxx` 命名。
|
| 87 |
-
- 用户要求修改完成后,默认不仅推送 GitHub 分支,也要同步提交到 Hugging Face Space,并完成可用的 merge / 部署路径;不要只停在本地或 GitHub 分支。
|
| 88 |
-
- 同步 HF Space 前先确认 `space` remote 指向 `build-small-hackathon/dream-customs`,并复核当前分支与提交 SHA,避免误推到其他 Space 或父级仓库。
|
| 89 |
-
- 如果 HF Space 同步涉及密钥、账单、权限弹窗、强制覆盖公开 `main`、或无法自动 merge 的 PR/Discussion,则先说明具体阻塞并请求用户确认;不要打印或保存任何 token。
|
| 90 |
-
|
| 91 |
-
## 实现原则
|
| 92 |
-
|
| 93 |
-
- 先用 fake clients 打通 schema、safety、render、pipeline 和 Gradio UI。
|
| 94 |
-
- 核心测试通过后再接真实 MiniCPM 模型。
|
| 95 |
-
- 所有模型输出都必须经过 schema 校验。
|
| 96 |
-
- JSON 解析失败时必须有修复或降级路径。
|
| 97 |
-
- Text-only path 必须永远可用,作为 demo fallback。
|
| 98 |
-
- Gradio 页面应优先移动端可用,因为用户可能在��机上记录梦。
|
| 99 |
-
- 今日 Tips 必须有梦境锚点,不能是“多休息、多喝水、保持积极”这类脱离梦境的通用话。
|
| 100 |
-
|
| 101 |
-
## 验收命令
|
| 102 |
-
|
| 103 |
-
基础验收:
|
| 104 |
-
|
| 105 |
-
```bash
|
| 106 |
-
python -m pytest -q
|
| 107 |
-
python app.py
|
| 108 |
-
```
|
| 109 |
-
|
| 110 |
-
人工验收:
|
| 111 |
-
|
| 112 |
-
- 输入一段文字梦境后,app 能提出至少一个相关追问。
|
| 113 |
-
- 用户可以回答追问,也可以跳过。
|
| 114 |
-
- 上传一张草图或便签后,视觉线索进入同一个 intake。
|
| 115 |
-
- 上传/录制语音后,有转写或清晰降级提示。
|
| 116 |
-
- 最终输出包含梦境摘要、解读草稿和一个今日小 Tips。
|
| 117 |
-
- 今日小 Tips 引用至少一个具体梦境细节。
|
| 118 |
-
- 输出不包含诊断、恐吓、预言或医疗化建议。
|
| 119 |
-
- 页面在手机宽度下可读。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dream QA Agent Guide
|
| 2 |
+
|
| 3 |
+
This file is the agent-facing entry point for the Dream QA / Dream Customs Space.
|
| 4 |
+
|
| 5 |
+
```bash
|
| 6 |
+
curl https://huggingface.co/spaces/build-small-hackathon/dream-customs/agents.md
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## App
|
| 10 |
+
|
| 11 |
+
- Public Space: https://huggingface.co/spaces/build-small-hackathon/dream-customs
|
| 12 |
+
- Runtime URL: https://build-small-hackathon-dream-customs.hf.space
|
| 13 |
+
- Product name: Dream QA
|
| 14 |
+
- Task: turn a dream fragment into one grounded follow-up question and one gentle Today Tip.
|
| 15 |
+
|
| 16 |
+
Dream QA is not a diagnosis, therapy, prophecy, or emergency tool. Treat every result as a soft reflection.
|
| 17 |
+
|
| 18 |
+
## Recommended Agent API
|
| 19 |
+
|
| 20 |
+
Use the text-only Gradio API endpoint:
|
| 21 |
+
|
| 22 |
+
```python
|
| 23 |
+
from gradio_client import Client
|
| 24 |
+
|
| 25 |
+
client = Client("https://build-small-hackathon-dream-customs.hf.space")
|
| 26 |
+
result = client.predict(
|
| 27 |
+
"I dreamed the elevator button melted and the number stayed on 14.",
|
| 28 |
+
"Uneasy",
|
| 29 |
+
"It felt like being late before I had even started.",
|
| 30 |
+
"en",
|
| 31 |
+
api_name="/agent_dream_qa",
|
| 32 |
+
)
|
| 33 |
+
print(result["card_text"])
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Input order:
|
| 37 |
+
|
| 38 |
+
1. `dream_text`: required dream fragment.
|
| 39 |
+
2. `mood`: optional waking mood, for example `Neutral`, `Uneasy`, `Foggy`, `Curious`, `Tired`, `A little amused`.
|
| 40 |
+
3. `answer`: optional answer to the follow-up question. Leave it empty to let Dream QA skip and generate a tip from the dream clues.
|
| 41 |
+
4. `language`: `en` or `zh`.
|
| 42 |
+
|
| 43 |
+
Expected output is a JSON object with:
|
| 44 |
+
|
| 45 |
+
- `status`: usually `tip` after a complete API call.
|
| 46 |
+
- `question`: the grounded follow-up question generated from the dream.
|
| 47 |
+
- `card_title`: `Today Tip` or `今日小 Tips`.
|
| 48 |
+
- `card_text`: copyable result text.
|
| 49 |
+
- `card_html`: rendered result card.
|
| 50 |
+
- `debug`: backend route and session state without secrets.
|
| 51 |
+
|
| 52 |
+
## Evaluation Checklist
|
| 53 |
+
|
| 54 |
+
An agent should consider the app healthy when all of these pass:
|
| 55 |
+
|
| 56 |
+
- `curl https://huggingface.co/spaces/build-small-hackathon/dream-customs/agents.md` returns this guide, not `Entry not found`.
|
| 57 |
+
- `/config` title is `Dream QA`.
|
| 58 |
+
- Text generation, image understanding, and voice input default to `modal`.
|
| 59 |
+
- `/config` exposes `agent_dream_qa` and does not require image/audio inputs for the public agent API.
|
| 60 |
+
- A sample dream reaches `status == "tip"`.
|
| 61 |
+
- The Today Tip mentions at least one concrete dream anchor.
|
| 62 |
+
- The result avoids diagnosis, prophecy, fate claims, and frightening certainty.
|
| 63 |
+
|
| 64 |
+
## Good Smoke Prompt
|
| 65 |
+
|
| 66 |
+
```text
|
| 67 |
+
Dream: I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14.
|
| 68 |
+
Mood: Uneasy
|
| 69 |
+
Answer: It felt like being late before I had even started.
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
The result should mention the elevator, button, or 14, and should suggest one small first step rather than a hard command.
|
dream_customs/ui/app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import json
|
|
|
|
| 2 |
from html import escape
|
| 3 |
|
| 4 |
import gradio as gr
|
|
@@ -58,6 +59,22 @@ def _json_schema_to_python_type(schema, defs):
|
|
| 58 |
|
| 59 |
gradio_client_utils._json_schema_to_python_type = _json_schema_to_python_type
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
VOICE_JS = r"""
|
| 62 |
() => {
|
| 63 |
const bindVoiceButton = () => {
|
|
@@ -441,6 +458,46 @@ def _reset(language, text_backend, vision_backend, *settings_values):
|
|
| 441 |
return (*_updates(state, view_json), "", "", None, None, default_mood_for(language))
|
| 442 |
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
def _active_step_for_status(status: str) -> int:
|
| 445 |
return {"record": 1, "error": 1, "ask": 2, "drafting": 3, "tip": 4}.get(status, 1)
|
| 446 |
|
|
@@ -605,6 +662,7 @@ def build_demo() -> gr.Blocks:
|
|
| 605 |
height=180,
|
| 606 |
elem_classes=["dc-image-popover"],
|
| 607 |
)
|
|
|
|
| 608 |
audio_input = gr.Audio(
|
| 609 |
label=initial_copy["voice_label"],
|
| 610 |
sources=["upload"],
|
|
@@ -613,6 +671,7 @@ def build_demo() -> gr.Blocks:
|
|
| 613 |
elem_classes=["dc-voice-input"],
|
| 614 |
visible=False,
|
| 615 |
)
|
|
|
|
| 616 |
field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
|
| 617 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 618 |
example_button = gr.Button(initial_copy["example_button"], variant="secondary")
|
|
@@ -753,6 +812,14 @@ def build_demo() -> gr.Blocks:
|
|
| 753 |
precision=0,
|
| 754 |
)
|
| 755 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
outputs = [
|
| 757 |
session_state,
|
| 758 |
view_state,
|
|
@@ -791,39 +858,51 @@ def build_demo() -> gr.Blocks:
|
|
| 791 |
_submit,
|
| 792 |
inputs=[dream_text, image_input, audio_input, mood, language, text_backend, vision_backend] + settings_inputs,
|
| 793 |
outputs=outputs,
|
|
|
|
|
|
|
| 794 |
)
|
| 795 |
answer_button.click(
|
| 796 |
_answer,
|
| 797 |
inputs=[session_state, answer_text, language, text_backend, vision_backend] + settings_inputs,
|
| 798 |
outputs=outputs,
|
|
|
|
|
|
|
| 799 |
)
|
| 800 |
skip_button.click(
|
| 801 |
_skip,
|
| 802 |
inputs=[session_state, language, text_backend, vision_backend] + settings_inputs,
|
| 803 |
outputs=outputs,
|
|
|
|
|
|
|
| 804 |
)
|
| 805 |
gentle_button.click(
|
| 806 |
_revise,
|
| 807 |
inputs=[session_state, gr.State("softer"), language, text_backend, vision_backend] + settings_inputs,
|
| 808 |
outputs=outputs,
|
|
|
|
|
|
|
| 809 |
)
|
| 810 |
weird_button.click(
|
| 811 |
_revise,
|
| 812 |
inputs=[session_state, gr.State("stranger"), language, text_backend, vision_backend] + settings_inputs,
|
| 813 |
outputs=outputs,
|
|
|
|
|
|
|
| 814 |
)
|
| 815 |
-
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text)
|
| 816 |
reset_button.click(
|
| 817 |
_reset,
|
| 818 |
inputs=[language, text_backend, vision_backend] + settings_inputs,
|
| 819 |
outputs=outputs + [dream_text, answer_text, image_input, audio_input, mood],
|
|
|
|
|
|
|
| 820 |
)
|
| 821 |
|
| 822 |
def _example(selected_language):
|
| 823 |
selected_language = normalize_language(selected_language)
|
| 824 |
return EXAMPLE_DREAMS[selected_language], EXAMPLE_MOODS[selected_language]
|
| 825 |
|
| 826 |
-
example_button.click(_example, inputs=language, outputs=[dream_text, mood])
|
| 827 |
|
| 828 |
def _language_ui(selected_language):
|
| 829 |
selected_language = normalize_language(selected_language)
|
|
@@ -893,6 +972,15 @@ def build_demo() -> gr.Blocks:
|
|
| 893 |
debug_help_html,
|
| 894 |
debug_json,
|
| 895 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
)
|
| 897 |
|
| 898 |
return demo
|
|
|
|
| 1 |
import json
|
| 2 |
+
from copy import deepcopy
|
| 3 |
from html import escape
|
| 4 |
|
| 5 |
import gradio as gr
|
|
|
|
| 59 |
|
| 60 |
gradio_client_utils._json_schema_to_python_type = _json_schema_to_python_type
|
| 61 |
|
| 62 |
+
|
| 63 |
+
def _make_media_api_info_client_safe(component):
|
| 64 |
+
"""Patch Gradio FileData schemas so gradio_client can parse the config."""
|
| 65 |
+
|
| 66 |
+
original_api_info = component.api_info
|
| 67 |
+
|
| 68 |
+
def api_info():
|
| 69 |
+
schema = deepcopy(original_api_info())
|
| 70 |
+
meta = schema.get("properties", {}).get("meta", {})
|
| 71 |
+
if isinstance(meta.get("additionalProperties"), bool):
|
| 72 |
+
meta["additionalProperties"] = {"type": "string"}
|
| 73 |
+
return schema
|
| 74 |
+
|
| 75 |
+
component.api_info = api_info
|
| 76 |
+
|
| 77 |
+
|
| 78 |
VOICE_JS = r"""
|
| 79 |
() => {
|
| 80 |
const bindVoiceButton = () => {
|
|
|
|
| 458 |
return (*_updates(state, view_json), "", "", None, None, default_mood_for(language))
|
| 459 |
|
| 460 |
|
| 461 |
+
def _agent_dream_qa(dream_text: str, mood: str = "", answer: str = "", language: str = DEFAULT_LANGUAGE) -> dict:
|
| 462 |
+
"""Text-only public API for external agents.
|
| 463 |
+
|
| 464 |
+
The visual UI keeps image and voice inputs, but those components currently
|
| 465 |
+
generate Gradio schemas that some agent clients cannot parse. This endpoint
|
| 466 |
+
keeps the agent path simple and stable while still exercising the same
|
| 467 |
+
Dream QA pipeline and configured Modal text backend.
|
| 468 |
+
"""
|
| 469 |
+
|
| 470 |
+
language = normalize_language(language)
|
| 471 |
+
state, view_json = submit_dream_action(
|
| 472 |
+
dream_text=dream_text,
|
| 473 |
+
image_value=None,
|
| 474 |
+
audio_value=None,
|
| 475 |
+
mood=mood,
|
| 476 |
+
text_backend=DEFAULT_TEXT_BACKEND,
|
| 477 |
+
vision_backend=DEFAULT_VISION_BACKEND,
|
| 478 |
+
language=language,
|
| 479 |
+
)
|
| 480 |
+
view = json.loads(view_json)
|
| 481 |
+
if view.get("status") != "ask":
|
| 482 |
+
return view
|
| 483 |
+
if answer and answer.strip():
|
| 484 |
+
_state, view_json = answer_to_card_action(
|
| 485 |
+
state,
|
| 486 |
+
answer=answer,
|
| 487 |
+
text_backend=DEFAULT_TEXT_BACKEND,
|
| 488 |
+
vision_backend=DEFAULT_VISION_BACKEND,
|
| 489 |
+
language=language,
|
| 490 |
+
)
|
| 491 |
+
else:
|
| 492 |
+
_state, view_json = skip_to_card_action(
|
| 493 |
+
state,
|
| 494 |
+
text_backend=DEFAULT_TEXT_BACKEND,
|
| 495 |
+
vision_backend=DEFAULT_VISION_BACKEND,
|
| 496 |
+
language=language,
|
| 497 |
+
)
|
| 498 |
+
return json.loads(view_json)
|
| 499 |
+
|
| 500 |
+
|
| 501 |
def _active_step_for_status(status: str) -> int:
|
| 502 |
return {"record": 1, "error": 1, "ask": 2, "drafting": 3, "tip": 4}.get(status, 1)
|
| 503 |
|
|
|
|
| 662 |
height=180,
|
| 663 |
elem_classes=["dc-image-popover"],
|
| 664 |
)
|
| 665 |
+
_make_media_api_info_client_safe(image_input)
|
| 666 |
audio_input = gr.Audio(
|
| 667 |
label=initial_copy["voice_label"],
|
| 668 |
sources=["upload"],
|
|
|
|
| 671 |
elem_classes=["dc-voice-input"],
|
| 672 |
visible=False,
|
| 673 |
)
|
| 674 |
+
_make_media_api_info_client_safe(audio_input)
|
| 675 |
field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
|
| 676 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 677 |
example_button = gr.Button(initial_copy["example_button"], variant="secondary")
|
|
|
|
| 812 |
precision=0,
|
| 813 |
)
|
| 814 |
|
| 815 |
+
with gr.Group(visible=False):
|
| 816 |
+
agent_dream_text = gr.Textbox(label="Agent dream text")
|
| 817 |
+
agent_mood = gr.Textbox(label="Agent mood", value=DEFAULT_MOOD)
|
| 818 |
+
agent_answer = gr.Textbox(label="Agent answer")
|
| 819 |
+
agent_language = gr.Textbox(label="Agent language", value=DEFAULT_LANGUAGE)
|
| 820 |
+
agent_result = gr.JSON(label="Agent Dream QA result")
|
| 821 |
+
agent_button = gr.Button("Agent Dream QA")
|
| 822 |
+
|
| 823 |
outputs = [
|
| 824 |
session_state,
|
| 825 |
view_state,
|
|
|
|
| 858 |
_submit,
|
| 859 |
inputs=[dream_text, image_input, audio_input, mood, language, text_backend, vision_backend] + settings_inputs,
|
| 860 |
outputs=outputs,
|
| 861 |
+
api_name=False,
|
| 862 |
+
show_api=False,
|
| 863 |
)
|
| 864 |
answer_button.click(
|
| 865 |
_answer,
|
| 866 |
inputs=[session_state, answer_text, language, text_backend, vision_backend] + settings_inputs,
|
| 867 |
outputs=outputs,
|
| 868 |
+
api_name=False,
|
| 869 |
+
show_api=False,
|
| 870 |
)
|
| 871 |
skip_button.click(
|
| 872 |
_skip,
|
| 873 |
inputs=[session_state, language, text_backend, vision_backend] + settings_inputs,
|
| 874 |
outputs=outputs,
|
| 875 |
+
api_name=False,
|
| 876 |
+
show_api=False,
|
| 877 |
)
|
| 878 |
gentle_button.click(
|
| 879 |
_revise,
|
| 880 |
inputs=[session_state, gr.State("softer"), language, text_backend, vision_backend] + settings_inputs,
|
| 881 |
outputs=outputs,
|
| 882 |
+
api_name=False,
|
| 883 |
+
show_api=False,
|
| 884 |
)
|
| 885 |
weird_button.click(
|
| 886 |
_revise,
|
| 887 |
inputs=[session_state, gr.State("stranger"), language, text_backend, vision_backend] + settings_inputs,
|
| 888 |
outputs=outputs,
|
| 889 |
+
api_name=False,
|
| 890 |
+
show_api=False,
|
| 891 |
)
|
| 892 |
+
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text, api_name=False, show_api=False)
|
| 893 |
reset_button.click(
|
| 894 |
_reset,
|
| 895 |
inputs=[language, text_backend, vision_backend] + settings_inputs,
|
| 896 |
outputs=outputs + [dream_text, answer_text, image_input, audio_input, mood],
|
| 897 |
+
api_name=False,
|
| 898 |
+
show_api=False,
|
| 899 |
)
|
| 900 |
|
| 901 |
def _example(selected_language):
|
| 902 |
selected_language = normalize_language(selected_language)
|
| 903 |
return EXAMPLE_DREAMS[selected_language], EXAMPLE_MOODS[selected_language]
|
| 904 |
|
| 905 |
+
example_button.click(_example, inputs=language, outputs=[dream_text, mood], api_name=False, show_api=False)
|
| 906 |
|
| 907 |
def _language_ui(selected_language):
|
| 908 |
selected_language = normalize_language(selected_language)
|
|
|
|
| 972 |
debug_help_html,
|
| 973 |
debug_json,
|
| 974 |
],
|
| 975 |
+
api_name=False,
|
| 976 |
+
show_api=False,
|
| 977 |
+
)
|
| 978 |
+
agent_button.click(
|
| 979 |
+
_agent_dream_qa,
|
| 980 |
+
inputs=[agent_dream_text, agent_mood, agent_answer, agent_language],
|
| 981 |
+
outputs=agent_result,
|
| 982 |
+
api_name="agent_dream_qa",
|
| 983 |
+
show_api=True,
|
| 984 |
)
|
| 985 |
|
| 986 |
return demo
|
scripts/evaluate_agent_readiness.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate whether Dream QA is ready for external agents."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
from urllib.error import URLError
|
| 13 |
+
from urllib.request import urlopen
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 16 |
+
if str(ROOT) not in sys.path:
|
| 17 |
+
sys.path.insert(0, str(ROOT))
|
| 18 |
+
|
| 19 |
+
from scripts.evaluate_today_tip_quality import evaluate_cases, _load_cases
|
| 20 |
+
from scripts.smoke_local_space_mirror import inspect_config
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
DEFAULT_APP_URL = "http://127.0.0.1:7862"
|
| 24 |
+
DEFAULT_AGENTS_URL = "https://huggingface.co/spaces/build-small-hackathon/dream-customs/agents.md"
|
| 25 |
+
REQUIRED_AGENT_DOC_TERMS = [
|
| 26 |
+
"Dream QA",
|
| 27 |
+
"agent_dream_qa",
|
| 28 |
+
"gradio_client",
|
| 29 |
+
"Today Tip",
|
| 30 |
+
"not a diagnosis",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _read_url(url: str, timeout: float) -> str:
|
| 35 |
+
with urlopen(url, timeout=timeout) as response:
|
| 36 |
+
return response.read().decode("utf-8", errors="replace")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _read_local_agents_doc(path: str) -> str:
|
| 40 |
+
text = Path(path).read_text(encoding="utf-8")
|
| 41 |
+
if Path(path).name == "agents.md" and "Dream QA Agent Guide" not in text:
|
| 42 |
+
staged = subprocess.run(
|
| 43 |
+
["git", "show", ":agents.md"],
|
| 44 |
+
check=False,
|
| 45 |
+
capture_output=True,
|
| 46 |
+
text=True,
|
| 47 |
+
)
|
| 48 |
+
if staged.returncode == 0 and "Dream QA Agent Guide" in staged.stdout:
|
| 49 |
+
return staged.stdout
|
| 50 |
+
return text
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _load_agents_doc(path: str | None, url: str | None, timeout: float) -> dict[str, Any]:
|
| 54 |
+
source = path or url
|
| 55 |
+
try:
|
| 56 |
+
text = _read_local_agents_doc(path) if path else _read_url(str(url), timeout)
|
| 57 |
+
except (OSError, URLError, TimeoutError) as exc:
|
| 58 |
+
return {"source": source, "passes": False, "issues": [f"unreadable:{type(exc).__name__}"]}
|
| 59 |
+
|
| 60 |
+
lowered = text.lower()
|
| 61 |
+
issues = []
|
| 62 |
+
if lowered.strip() == "entry not found":
|
| 63 |
+
issues.append("entry_not_found")
|
| 64 |
+
for term in REQUIRED_AGENT_DOC_TERMS:
|
| 65 |
+
if term.lower() not in lowered:
|
| 66 |
+
issues.append(f"missing:{term}")
|
| 67 |
+
return {
|
| 68 |
+
"source": source,
|
| 69 |
+
"passes": not issues,
|
| 70 |
+
"issues": issues,
|
| 71 |
+
"bytes": len(text.encode("utf-8")),
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _fetch_config(app_url: str, timeout: float) -> dict[str, Any]:
|
| 76 |
+
return json.loads(_read_url(f"{app_url.rstrip('/')}/config", timeout))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _agent_api_result(config: dict[str, Any]) -> dict[str, Any]:
|
| 80 |
+
dependencies = config.get("dependencies", [])
|
| 81 |
+
public_api_names = [item.get("api_name") for item in dependencies if item.get("api_name")]
|
| 82 |
+
agent_deps = [item for item in dependencies if item.get("api_name") == "agent_dream_qa"]
|
| 83 |
+
issues = []
|
| 84 |
+
if not agent_deps:
|
| 85 |
+
issues.append("missing_agent_dream_qa")
|
| 86 |
+
if any(name in {"_submit", "_answer", "_skip"} for name in public_api_names):
|
| 87 |
+
issues.append("ui_events_exposed_as_public_api")
|
| 88 |
+
|
| 89 |
+
components_by_id = {component.get("id"): component for component in config.get("components", [])}
|
| 90 |
+
if agent_deps:
|
| 91 |
+
input_types = [
|
| 92 |
+
components_by_id.get(component_id, {}).get("type")
|
| 93 |
+
for component_id in agent_deps[0].get("inputs", [])
|
| 94 |
+
]
|
| 95 |
+
if any(kind in {"image", "audio"} for kind in input_types):
|
| 96 |
+
issues.append("agent_api_requires_media_schema")
|
| 97 |
+
else:
|
| 98 |
+
input_types = []
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"passes": not issues,
|
| 102 |
+
"issues": issues,
|
| 103 |
+
"public_api_names": public_api_names,
|
| 104 |
+
"agent_input_types": input_types,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _gradio_client_result(app_url: str, timeout: float) -> dict[str, Any]:
|
| 109 |
+
try:
|
| 110 |
+
from gradio_client import Client
|
| 111 |
+
|
| 112 |
+
client = Client(app_url)
|
| 113 |
+
result = client.predict(
|
| 114 |
+
"I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14.",
|
| 115 |
+
"Uneasy",
|
| 116 |
+
"It felt like being late before I had even started.",
|
| 117 |
+
"en",
|
| 118 |
+
api_name="/agent_dream_qa",
|
| 119 |
+
)
|
| 120 |
+
except Exception as exc: # pragma: no cover - exercised by smoke command
|
| 121 |
+
return {"passes": False, "issues": [f"{type(exc).__name__}:{str(exc).splitlines()[0]}"]}
|
| 122 |
+
|
| 123 |
+
text = json.dumps(result, ensure_ascii=False).lower()
|
| 124 |
+
issues = []
|
| 125 |
+
if result.get("status") != "tip":
|
| 126 |
+
issues.append(f"status:{result.get('status')}")
|
| 127 |
+
if not any(anchor in text for anchor in ["elevator", "button", "14"]):
|
| 128 |
+
issues.append("missing_anchor")
|
| 129 |
+
unsafe_terms = [
|
| 130 |
+
"you have depression",
|
| 131 |
+
"this dream proves",
|
| 132 |
+
"this means you will",
|
| 133 |
+
"prophecy says",
|
| 134 |
+
"fate says",
|
| 135 |
+
"you will fail",
|
| 136 |
+
]
|
| 137 |
+
if any(term in text for term in unsafe_terms):
|
| 138 |
+
issues.append("unsafe_language")
|
| 139 |
+
return {"passes": not issues, "issues": issues, "status": result.get("status")}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def evaluate(app_url: str, agents_path: str | None, agents_url: str | None, run_client: bool, timeout: float) -> dict[str, Any]:
|
| 143 |
+
config = _fetch_config(app_url, timeout)
|
| 144 |
+
config_result = inspect_config(config)
|
| 145 |
+
report = {
|
| 146 |
+
"app_url": app_url,
|
| 147 |
+
"agents_doc": _load_agents_doc(agents_path, agents_url, timeout),
|
| 148 |
+
"space_config": config_result,
|
| 149 |
+
"agent_api": _agent_api_result(config),
|
| 150 |
+
"today_tip_quality": evaluate_cases(_load_cases()),
|
| 151 |
+
}
|
| 152 |
+
if run_client:
|
| 153 |
+
report["gradio_client"] = _gradio_client_result(app_url, timeout)
|
| 154 |
+
report["passes"] = all(section.get("passes") for section in report.values() if isinstance(section, dict))
|
| 155 |
+
return report
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
| 159 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 160 |
+
parser.add_argument("--app-url", default=DEFAULT_APP_URL)
|
| 161 |
+
parser.add_argument("--agents-path", default="agents.md")
|
| 162 |
+
parser.add_argument("--agents-url", default=DEFAULT_AGENTS_URL)
|
| 163 |
+
parser.add_argument("--remote-agents-doc", action="store_true", help="Read agents.md from --agents-url instead of local path.")
|
| 164 |
+
parser.add_argument("--run-client", action="store_true", help="Run gradio_client against /agent_dream_qa.")
|
| 165 |
+
parser.add_argument("--timeout", type=float, default=30.0)
|
| 166 |
+
return parser.parse_args(argv)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def main(argv: list[str] | None = None) -> int:
|
| 170 |
+
args = parse_args(argv)
|
| 171 |
+
report = evaluate(
|
| 172 |
+
app_url=args.app_url,
|
| 173 |
+
agents_path=None if args.remote_agents_doc else args.agents_path,
|
| 174 |
+
agents_url=args.agents_url,
|
| 175 |
+
run_client=args.run_client,
|
| 176 |
+
timeout=args.timeout,
|
| 177 |
+
)
|
| 178 |
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
| 179 |
+
return 0 if report["passes"] else 1
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
if __name__ == "__main__":
|
| 183 |
+
raise SystemExit(main())
|
tests/test_agent_readiness.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from scripts.evaluate_agent_readiness import _agent_api_result, _load_agents_doc
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_agents_doc_is_present_and_agent_facing():
|
| 5 |
+
result = _load_agents_doc("agents.md", None, timeout=1)
|
| 6 |
+
|
| 7 |
+
assert result["passes"] is True
|
| 8 |
+
assert result["issues"] == []
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_agent_api_requires_text_only_public_endpoint():
|
| 12 |
+
config = {
|
| 13 |
+
"components": [
|
| 14 |
+
{"id": 1, "type": "textbox"},
|
| 15 |
+
{"id": 2, "type": "textbox"},
|
| 16 |
+
{"id": 3, "type": "textbox"},
|
| 17 |
+
{"id": 4, "type": "textbox"},
|
| 18 |
+
{"id": 5, "type": "json"},
|
| 19 |
+
],
|
| 20 |
+
"dependencies": [
|
| 21 |
+
{
|
| 22 |
+
"api_name": "agent_dream_qa",
|
| 23 |
+
"inputs": [1, 2, 3, 4],
|
| 24 |
+
"outputs": [5],
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
result = _agent_api_result(config)
|
| 30 |
+
|
| 31 |
+
assert result["passes"] is True
|
| 32 |
+
assert result["agent_input_types"] == ["textbox", "textbox", "textbox", "textbox"]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_agent_api_fails_if_media_schema_is_public():
|
| 36 |
+
config = {
|
| 37 |
+
"components": [
|
| 38 |
+
{"id": 1, "type": "textbox"},
|
| 39 |
+
{"id": 2, "type": "image"},
|
| 40 |
+
{"id": 3, "type": "json"},
|
| 41 |
+
],
|
| 42 |
+
"dependencies": [
|
| 43 |
+
{
|
| 44 |
+
"api_name": "agent_dream_qa",
|
| 45 |
+
"inputs": [1, 2],
|
| 46 |
+
"outputs": [3],
|
| 47 |
+
},
|
| 48 |
+
{"api_name": "_submit", "inputs": [1, 2], "outputs": [3]},
|
| 49 |
+
],
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
result = _agent_api_result(config)
|
| 53 |
+
|
| 54 |
+
assert result["passes"] is False
|
| 55 |
+
assert "agent_api_requires_media_schema" in result["issues"]
|
| 56 |
+
assert "ui_events_exposed_as_public_api" in result["issues"]
|
tests/test_ui_actions.py
CHANGED
|
@@ -36,6 +36,7 @@ def test_voice_input_keeps_modal_asr_component_but_uses_inline_mic_button():
|
|
| 36 |
assert 'sources=["microphone", "upload"]' not in source
|
| 37 |
assert 'type="filepath"' in source
|
| 38 |
assert "visible=False" in source
|
|
|
|
| 39 |
assert "mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))" in source
|
| 40 |
assert "audio_input = gr.State(None)" not in source
|
| 41 |
assert 'value=DEFAULT_ASR_BACKEND' in source
|
|
@@ -48,6 +49,7 @@ def test_image_upload_is_composer_plus_drawer():
|
|
| 48 |
assert 'elem_classes=["dc-image-popover"]' in source
|
| 49 |
assert 'image_input = gr.Image(' in source
|
| 50 |
assert 'sources=["upload", "clipboard"]' in source
|
|
|
|
| 51 |
assert "gr.Accordion(" not in source.split("image_input = gr.Image(")[0].split("with gr.Group(elem_classes=[\"dc-composer\"]):")[-1]
|
| 52 |
|
| 53 |
|
|
|
|
| 36 |
assert 'sources=["microphone", "upload"]' not in source
|
| 37 |
assert 'type="filepath"' in source
|
| 38 |
assert "visible=False" in source
|
| 39 |
+
assert "_make_media_api_info_client_safe(audio_input)" in source
|
| 40 |
assert "mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))" in source
|
| 41 |
assert "audio_input = gr.State(None)" not in source
|
| 42 |
assert 'value=DEFAULT_ASR_BACKEND' in source
|
|
|
|
| 49 |
assert 'elem_classes=["dc-image-popover"]' in source
|
| 50 |
assert 'image_input = gr.Image(' in source
|
| 51 |
assert 'sources=["upload", "clipboard"]' in source
|
| 52 |
+
assert "_make_media_api_info_client_safe(image_input)" in source
|
| 53 |
assert "gr.Accordion(" not in source.split("image_input = gr.Image(")[0].split("with gr.Group(elem_classes=[\"dc-composer\"]):")[-1]
|
| 54 |
|
| 55 |
|