Deploy current gen-ui build with fast-agent 0.6.1
Browse files- .prefab/agent-cards/_monty_codegen_shared.md +609 -0
- .prefab/agent-cards/_prefab_wire_shared.md +447 -0
- .prefab/agent-cards/hub_search_prefab_llm_chain.md +12 -0
- .prefab/agent-cards/hub_search_prefab_llm_codegen.md +117 -0
- .prefab/agent-cards/hub_search_prefab_llm_raw.md +44 -0
- .prefab/agent-cards/hub_search_prefab_native.md +89 -0
- .prefab/agent-cards/hub_search_raw.md +40 -0
- .prefab/fastagent.config.yaml +11 -0
- .prefab/tool-cards/monty_api_tool_v2.py +43 -0
- .prod/tool-cards/monty_api_tool_v2.py +0 -0
- Dockerfile +31 -0
- README.md +17 -6
- scripts/hub_search_prefab_server.py +208 -0
- scripts/prefab_hub_ui.py +1008 -0
.prefab/agent-cards/_monty_codegen_shared.md
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Runtime rules for generated code
|
| 2 |
+
|
| 3 |
+
- You **MUST NOT** use any imports.
|
| 4 |
+
- All helper functions are already in scope.
|
| 5 |
+
- All helper/API calls are async: always use `await`.
|
| 6 |
+
- `max_calls` is the total external-call budget for the whole generated program, not a generic helper argument.
|
| 7 |
+
- The outer wrapper is an exact contract. You **MUST** use this exact skeleton and only change the body:
|
| 8 |
+
|
| 9 |
+
```py
|
| 10 |
+
async def solve(query, max_calls):
|
| 11 |
+
...
|
| 12 |
+
# body goes here
|
| 13 |
+
|
| 14 |
+
await solve(query, max_calls)
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
- Always prefer helper functions. Use `call_api('/api/...')` only when no helper fits.
|
| 18 |
+
- `call_api` must receive a raw path starting with `/api/...`; never call helper names through `call_api`.
|
| 19 |
+
- `call_api(...)` returns `{ok, status, url, data, error}`. Always check `resp["ok"]` before reading `resp["data"]`. Do not read `resp["items"]` or `resp["meta"]` directly from `call_api(...)`.
|
| 20 |
+
- `call_api(...)` only accepts `endpoint`, `params`, `method`, and `json_body`. Do not guess extra kwargs.
|
| 21 |
+
- Use `call_api(...)` only for endpoint families that do not already have a helper, such as tag metadata endpoints.
|
| 22 |
+
- For questions about supported helpers, fields, limits, raw API affordances, or runtime capabilities, use `hf_runtime_capabilities(...)` instead of hand-authoring a static answer from memory.
|
| 23 |
+
- Keep final displayed results compact, but do not artificially shrink intermediate helper coverage unless the user explicitly asked for a sample.
|
| 24 |
+
- Prefer canonical snake_case keys in generated code and in JSON output.
|
| 25 |
+
- When returning a structured dict that includes your own coverage metadata, use the exact top-level keys `results` and `coverage` unless the user explicitly requested different key names.
|
| 26 |
+
- Omit unavailable optional fields instead of emitting `null` placeholders unless the user explicitly asked for a fixed schema with nulls.
|
| 27 |
+
- If the user asks for specific fields or says "return only", return exactly that final shape from `solve(...)`.
|
| 28 |
+
- For current-user prompts (`my`, `me`), use helpers with `username=None` first. Only ask for identity if that fails.
|
| 29 |
+
- When a current-user helper response has `ok=false`, return that helper response directly instead of flattening it into an empty result.
|
| 30 |
+
|
| 31 |
+
## Common helper signature traps
|
| 32 |
+
These are high-priority rules. Do not guess helper arguments.
|
| 33 |
+
|
| 34 |
+
- `hf_repo_search(...)` uses `limit`, **not** `return_limit`, and does **not** accept `count_only`.
|
| 35 |
+
- `hf_trending(...)` uses `limit`, **not** `return_limit`.
|
| 36 |
+
- `hf_daily_papers(...)` uses `limit`, **not** `return_limit`.
|
| 37 |
+
- `hf_repo_discussions(...)` uses `limit`, **not** `return_limit`.
|
| 38 |
+
- `hf_user_graph(...)`, `hf_user_likes(...)`, `hf_org_members(...)`, `hf_recent_activity(...)`, and `hf_collection_items(...)` use `return_limit`.
|
| 39 |
+
- `hf_profile_summary(include=...)` supports only `"likes"` and `"activity"`.
|
| 40 |
+
- Do **not** guess `hf_profile_summary(include=[...])` values such as `"followers"`, `"following"`, `"models"`, `"datasets"`, or `"spaces"`.
|
| 41 |
+
- `followers_count`, `following_count`, `models_count`, `datasets_count`, `spaces_count`, and similar aggregate counts already come from the base `hf_profile_summary(...)["item"]`.
|
| 42 |
+
- `return_limit=None` does **not** mean exhaustive or "all rows". It means the helper uses its documented default.
|
| 43 |
+
- When `count_only=True`, omit `return_limit`; count-only requests ignore row-return limits and return no items.
|
| 44 |
+
- For "how many models/datasets/spaces does org/user X have?" prefer `hf_profile_summary(...)["item"]` instead of trying to count with `hf_repo_search(...)`.
|
| 45 |
+
- Never invent helper args such as `count_only=True` for helpers that do not document it.
|
| 46 |
+
|
| 47 |
+
## Helper result shape
|
| 48 |
+
All helpers return:
|
| 49 |
+
```py
|
| 50 |
+
{
|
| 51 |
+
"ok": bool,
|
| 52 |
+
"item": dict | None,
|
| 53 |
+
"items": list[dict],
|
| 54 |
+
"meta": dict,
|
| 55 |
+
"error": str | None,
|
| 56 |
+
}
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Rules:
|
| 60 |
+
- `items` is the canonical list field.
|
| 61 |
+
- `item` is only a singleton convenience.
|
| 62 |
+
- `meta` contains helper-owned execution, coverage, and limit information.
|
| 63 |
+
- For metadata-oriented prompts, return the relevant `meta` fields instead of inferring coverage from list length alone.
|
| 64 |
+
- For bounded list/sample helpers in raw mode, returning the helper envelope directly preserves helper-owned `meta` fields.
|
| 65 |
+
|
| 66 |
+
## Routing guide
|
| 67 |
+
|
| 68 |
+
### Summary vs detail
|
| 69 |
+
- Summary helpers are the default for list/search/trending questions: `hf_repo_search(...)`, `hf_trending(...)`, `hf_daily_papers(...)`, `hf_user_likes(...)`, `hf_recent_activity(...)`, `hf_collections_search(...)`, `hf_collection_items(...)`, `hf_org_members(...)`, `hf_user_graph(...)`.
|
| 70 |
+
- Use `hf_repo_details(...)` when the user needs exact repo metadata rather than a cheap summary row.
|
| 71 |
+
- Do **not** invent follow-up detail calls unless the user explicitly needs fields that are not already available in the current helper response.
|
| 72 |
+
|
| 73 |
+
### Runtime self-description
|
| 74 |
+
- Supported helpers / default fields / limits / raw API affordances → `hf_runtime_capabilities(...)`
|
| 75 |
+
- If the question is specifically about helper defaults or cost behavior, prefer `hf_runtime_capabilities(section="helper_defaults")`.
|
| 76 |
+
|
| 77 |
+
### Repo questions
|
| 78 |
+
- Exact `owner/name` details → `hf_repo_details(repo_type="auto", ...)`
|
| 79 |
+
- Search/discovery/list/top repos → `hf_repo_search(...)`
|
| 80 |
+
- True trending requests → `hf_trending(...)`
|
| 81 |
+
- Daily papers → `hf_daily_papers(...)`
|
| 82 |
+
- Repo discussions → `hf_repo_discussions(...)`
|
| 83 |
+
- Specific discussion details / latest comment text → `hf_repo_discussion_details(...)`
|
| 84 |
+
- Users who liked a specific repo → `hf_repo_likers(...)`
|
| 85 |
+
|
| 86 |
+
### User questions
|
| 87 |
+
- Profile / overview / "tell me about user X" → `hf_profile_summary(...)`
|
| 88 |
+
- Follower/following **counts** for a user → prefer `hf_profile_summary(...)`
|
| 89 |
+
- Followers / following **lists**, graph samples, and social joins → `hf_user_graph(...)`
|
| 90 |
+
- Repos a user liked → `hf_user_likes(...)`
|
| 91 |
+
- Recent actions / activity feed → `hf_recent_activity(feed_type="user", entity=...)`
|
| 92 |
+
|
| 93 |
+
### Organization questions
|
| 94 |
+
- Organization details and counts → `hf_profile_summary(...)`
|
| 95 |
+
- Organization members → `hf_org_members(...)`
|
| 96 |
+
- Organization repos → `hf_repo_search(author="<org>", repo_types=[...])`
|
| 97 |
+
- Organization or user collections → `hf_collections_search(owner="<org-or-user>", ...)`
|
| 98 |
+
- Repos inside a known collection → `hf_collection_items(collection_id=...)`
|
| 99 |
+
|
| 100 |
+
### Direction reminders
|
| 101 |
+
- `hf_user_likes(...)` = **user → repos**
|
| 102 |
+
- `hf_repo_likers(...)` = **repo → users**
|
| 103 |
+
- `hf_user_graph(...)` = **user/org → followers/following**
|
| 104 |
+
- `"who follows X"` → `hf_user_graph(username="X", relation="followers", ...)`
|
| 105 |
+
- `"who does X follow"` → `hf_user_graph(username="X", relation="following", ...)`
|
| 106 |
+
- If the author/org is already known, start with `hf_repo_search(author=...)` instead of semantic search.
|
| 107 |
+
- For "most popular repo a user liked", use `hf_user_likes(sort="repoLikes" | "repoDownloads", ranking_window=40)` instead of fetching recent likes and re-ranking locally.
|
| 108 |
+
|
| 109 |
+
### Join / intersection guidance
|
| 110 |
+
- For set-intersection questions, prefer **one helper call per side + local set logic**.
|
| 111 |
+
- Example: `"who in the huggingface org follows evalstate"` should use:
|
| 112 |
+
1. `hf_org_members(organization="huggingface", ...)`
|
| 113 |
+
2. `hf_user_graph(username="evalstate", relation="followers", ...)`
|
| 114 |
+
3. intersect `username` locally
|
| 115 |
+
- Example: `"who in the huggingface org does evalstate follow"` should use:
|
| 116 |
+
1. `hf_org_members(organization="huggingface", ...)`
|
| 117 |
+
2. `hf_user_graph(username="evalstate", relation="following", ...)`
|
| 118 |
+
3. intersect `username` locally
|
| 119 |
+
- Do **not** invert follower/following direction when restating the prompt.
|
| 120 |
+
- Do **not** do one graph call per org member for these intersection questions unless you explicitly need a bounded fallback.
|
| 121 |
+
|
| 122 |
+
## Common row keys
|
| 123 |
+
Use these canonical keys unless the user explicitly wants different names.
|
| 124 |
+
|
| 125 |
+
- Repo rows: `repo_id`, `repo_type`, `author`, `likes`, `downloads`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `library_name`, `repo_url`, `tags`
|
| 126 |
+
- Daily paper rows: `paper_id`, `title`, `published_at`, `authors`, `organization`, `repo_id`, `rank`
|
| 127 |
+
- User graph/member rows: `username`, `fullname`, `isPro`, `role`, `type`
|
| 128 |
+
- Activity rows: `event_type`, `repo_id`, `repo_type`, `timestamp`
|
| 129 |
+
- Collection rows: `collection_id`, `slug`, `title`, `owner`, `owner_type`, `description`, `last_updated`, `item_count`
|
| 130 |
+
- `hf_profile_summary(...)["item"]`: `handle`, `entity_type`, `display_name`, `bio`, `description`, `avatar_url`, `website_url`, `twitter_url`, `github_url`, `linkedin_url`, `bluesky_url`, `followers_count`, `following_count`, `likes_count`, `members_count`, `models_count`, `datasets_count`, `spaces_count`, `is_pro`, `likes_sample`, `activity_sample`
|
| 131 |
+
|
| 132 |
+
Common aliases in `fields=[...]` are tolerated by the runtime, but prefer the canonical names above in generated code.
|
| 133 |
+
|
| 134 |
+
## Common repo fields
|
| 135 |
+
- `repo_id`
|
| 136 |
+
- `repo_type`
|
| 137 |
+
- `author`
|
| 138 |
+
- `likes`
|
| 139 |
+
- `downloads`
|
| 140 |
+
- `created_at`
|
| 141 |
+
- `last_modified`
|
| 142 |
+
- `pipeline_tag`
|
| 143 |
+
- `num_params`
|
| 144 |
+
- `repo_url`
|
| 145 |
+
- model: `library_name`
|
| 146 |
+
- dataset: `description`, `paperswithcode_id`
|
| 147 |
+
- space: `sdk`, `models`, `datasets`, `subdomain`
|
| 148 |
+
- trending: `trending_rank`, `trending_score` when present
|
| 149 |
+
- prefer `repo_id` as the display label for repos; `title` may be absent or may just mirror `repo_id`
|
| 150 |
+
|
| 151 |
+
Common aliases tolerated in `fields=[...]`:
|
| 152 |
+
- `repoId` → `repo_id`
|
| 153 |
+
- `repoType` → `repo_type`
|
| 154 |
+
- `repoUrl` → `repo_url`
|
| 155 |
+
- `createdAt` → `created_at`
|
| 156 |
+
- `lastModified` → `last_modified`
|
| 157 |
+
- `numParams` → `num_params`
|
| 158 |
+
|
| 159 |
+
## Common collection fields
|
| 160 |
+
- `collection_id`
|
| 161 |
+
- `slug`
|
| 162 |
+
- `title`
|
| 163 |
+
- `owner`
|
| 164 |
+
- `owner_type`
|
| 165 |
+
- `description`
|
| 166 |
+
- `last_updated`
|
| 167 |
+
- `item_count`
|
| 168 |
+
|
| 169 |
+
Common aliases tolerated in `fields=[...]`:
|
| 170 |
+
- `collectionId` → `collection_id`
|
| 171 |
+
- `lastUpdated` → `last_updated`
|
| 172 |
+
- `ownerType` → `owner_type`
|
| 173 |
+
- `itemCount` → `item_count`
|
| 174 |
+
- `author` → `owner`
|
| 175 |
+
|
| 176 |
+
## High-signal usage notes
|
| 177 |
+
- `hf_repo_search(...)` defaults to models if no repo type is specified. For prompts like "what repos does <author/org> have", search across `repo_types=["model", "dataset", "space"]` unless the user asked for one type.
|
| 178 |
+
- `hf_repo_search(...)` and `hf_trending(...)` are summary helpers. Use `hf_repo_details(...)` when the user explicitly needs exact repo metadata.
|
| 179 |
+
- For models, datasets, and spaces, do **not** rely on a separate repo `title` field in summary outputs. Prefer `repo_id` as the primary display key unless the user explicitly asked for another field and it is present.
|
| 180 |
+
- `hf_repo_search(...)` model rows may already include `num_params` when upstream metadata provides it. Use that cheap summary field before considering detail hydration.
|
| 181 |
+
- `hf_trending(...)` returns the Hub's ordered trending list as summary rows with `trending_rank`. `trending_score` may be present when the upstream payload provides it; never fabricate it.
|
| 182 |
+
- `hf_daily_papers(...)` is the normal path for today's daily papers. `repo_id` is optional there, so omit it when the helper row does not provide one.
|
| 183 |
+
- `hf_profile_summary(...)` is the fastest way to answer common profile prompts. Read profile/social fields directly from `summary["item"]`.
|
| 184 |
+
- For prompts like "how many followers do I have?" or "how many users does X follow?", prefer `hf_profile_summary(...)["item"]` for the aggregate count.
|
| 185 |
+
- For prompts like "who follows me?", "who does X follow?", or any follower/following intersection, use `hf_user_graph(...)` with the correct `relation`.
|
| 186 |
+
- For "how many models/datasets/spaces does user/org X have?" prompts, prefer `hf_profile_summary(...)["item"]` over `hf_repo_search(..., limit=1)` or invented `count_only` args.
|
| 187 |
+
- Use `hf_whoami()` when you need the explicit current username for joins, comparisons, or output labeling.
|
| 188 |
+
- For overlap/comparison/ranking/join tasks, fetch a broad enough **working set** first and compute locally in code.
|
| 189 |
+
- It is good to use a larger internal working set than the final user-facing output. Keep the **returned** results compact unless the user explicitly asked for a full dump.
|
| 190 |
+
- For completeness-sensitive joins over followers/members/likers, use an explicit large `return_limit` on the seed helpers rather than `return_limit=None`.
|
| 191 |
+
- Good pattern: use larger limits internally for coverage, then return only the compact final intersection/ranking/projection the user asked for.
|
| 192 |
+
- Avoid per-row hydration calls unless you truly need exact metadata that is not already present in the current helper response.
|
| 193 |
+
- For prompts that ask for both a sample and metadata, keep the sample compact and surface helper-owned `meta` fields explicitly.
|
| 194 |
+
- For follower/member social-link lookups, first fetch usernames with `hf_user_graph(...)` or `hf_org_members(...)`, then fetch profile/social data with `hf_profile_summary(handle=...)`.
|
| 195 |
+
- For fan-out tasks that require one helper call per follower/member/liker/repo/user, prefer bounded seed sets **by default** so ordinary requests stay fast and predictable.
|
| 196 |
+
- If the user explicitly asks for exhaustive coverage (`all`, `scan all`, `entire`, `not just the first N`, `ensure more than the first 20`, etc.), do **not** silently cap the seed at a small sample such as 20 or 50.
|
| 197 |
+
- For those explicit exhaustive requests, attempt a substantially broader seed scan first when the runtime budget permits.
|
| 198 |
+
- For explicit exhaustive follower/member scans, prefer omitting `return_limit` or using a value large enough to cover the expected total. Do **not** choose arbitrary small caps like 50 or 100 if that would obviously prevent an exhaustive answer.
|
| 199 |
+
- If the prompt says both `scan all` and `more than the first 20`, the `scan all` requirement wins. Do **not** satisfy that request with a bare sample of 50 unless you also mark the result as partial.
|
| 200 |
+
- If exhaustive coverage is still not feasible within `max_calls` or timeout, say so clearly and return an explicit partial result with coverage metadata instead of presenting a bounded sample as if it were complete.
|
| 201 |
+
- When you return a composed partial result, use the exact top-level keys `results` and `coverage` unless the user explicitly asked for a different schema. Do **not** rename `results` to `items`, `rows`, `liked_models`, or similar.
|
| 202 |
+
- Do **not** use your own top-level transport wrapper named `meta` in raw mode; runtime already owns the outer `meta`.
|
| 203 |
+
- Good coverage fields for partial fan-out results include: `partial`, `reason`, `seed_limit`, `seed_processed`, `seed_total`, `seed_more_available`, `per_entity_limit`, and `next_request_hint`.
|
| 204 |
+
- If the user did not explicitly require exhaustiveness, a clear partial result with coverage metadata is better than failing with `Max API calls exceeded`.
|
| 205 |
+
- If the user **did** explicitly require exhaustiveness and you cannot complete it, do not imply success. Report that the result is partial and include the relevant coverage/limit fields.
|
| 206 |
+
- For explicit exhaustive follower/member prompts, if `meta.more_available` is true or `seed_processed < seed_total`, the final output must not be a bare list that looks complete. Include explicit partial/coverage information.
|
| 207 |
+
- For compact join outputs, it is fine for the internal seed helpers to use larger limits than the final returned list. The user-facing output size and the internal working-set size are different concepts.
|
| 208 |
+
- Use `hf_recent_activity(...)` for activity feeds instead of raw `call_api('/api/recent-activity', ...)`.
|
| 209 |
+
- Use `hf_repo_search(author=..., repo_type="space", ...)` for Spaces by author; there is no separate spaces-by-author helper.
|
| 210 |
+
- Use `hf_collections_search(owner=...)` for "what collections does this org/user have?" prompts.
|
| 211 |
+
- `hf_collections_search(...)` is for finding/listing collections. It returns collection rows plus `item_count`, not the full repo rows inside each collection.
|
| 212 |
+
- Use `hf_collection_items(collection_id=...)` for "what repos/models/datasets/spaces are in this collection?" prompts.
|
| 213 |
+
- Do **not** guess raw collection item endpoints such as `/api/collections/.../items`.
|
| 214 |
+
|
| 215 |
+
## Helper API
|
| 216 |
+
```py
|
| 217 |
+
await hf_runtime_capabilities(section: str | None = None)
|
| 218 |
+
|
| 219 |
+
await hf_profile_summary(
|
| 220 |
+
handle: str | None = None,
|
| 221 |
+
include: list[str] | None = None,
|
| 222 |
+
likes_limit: int = 10,
|
| 223 |
+
activity_limit: int = 10,
|
| 224 |
+
)
|
| 225 |
+
# include supports only: ["likes"], ["activity"], or ["likes", "activity"]
|
| 226 |
+
# aggregate counts like followers_count / following_count / models_count are already in item
|
| 227 |
+
|
| 228 |
+
await hf_org_members(
|
| 229 |
+
organization: str,
|
| 230 |
+
return_limit: int | None = None,
|
| 231 |
+
scan_limit: int | None = None,
|
| 232 |
+
count_only: bool = False,
|
| 233 |
+
where: dict | None = None,
|
| 234 |
+
fields: list[str] | None = None,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
await hf_repo_search(
|
| 238 |
+
query: str | None = None,
|
| 239 |
+
repo_type: str | None = None,
|
| 240 |
+
repo_types: list[str] | None = None,
|
| 241 |
+
author: str | None = None,
|
| 242 |
+
filters: list[str] | None = None,
|
| 243 |
+
sort: str | None = None,
|
| 244 |
+
limit: int = 20,
|
| 245 |
+
where: dict | None = None,
|
| 246 |
+
fields: list[str] | None = None,
|
| 247 |
+
advanced: dict | None = None,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
await hf_repo_details(
|
| 251 |
+
repo_id: str | None = None,
|
| 252 |
+
repo_ids: list[str] | None = None,
|
| 253 |
+
repo_type: str = "auto",
|
| 254 |
+
fields: list[str] | None = None,
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
await hf_trending(
|
| 258 |
+
repo_type: str = "model",
|
| 259 |
+
limit: int = 20,
|
| 260 |
+
where: dict | None = None,
|
| 261 |
+
fields: list[str] | None = None,
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
await hf_daily_papers(
|
| 265 |
+
limit: int = 20,
|
| 266 |
+
where: dict | None = None,
|
| 267 |
+
fields: list[str] | None = None,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
await hf_user_graph(
|
| 271 |
+
username: str | None = None,
|
| 272 |
+
relation: str = "followers",
|
| 273 |
+
return_limit: int | None = None,
|
| 274 |
+
scan_limit: int | None = None,
|
| 275 |
+
count_only: bool = False,
|
| 276 |
+
pro_only: bool | None = None,
|
| 277 |
+
where: dict | None = None,
|
| 278 |
+
fields: list[str] | None = None,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
await hf_repo_likers(
|
| 282 |
+
repo_id: str,
|
| 283 |
+
repo_type: str,
|
| 284 |
+
return_limit: int | None = None,
|
| 285 |
+
count_only: bool = False,
|
| 286 |
+
pro_only: bool | None = None,
|
| 287 |
+
where: dict | None = None,
|
| 288 |
+
fields: list[str] | None = None,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
await hf_user_likes(
|
| 292 |
+
username: str | None = None,
|
| 293 |
+
repo_types: list[str] | None = None,
|
| 294 |
+
return_limit: int | None = None,
|
| 295 |
+
scan_limit: int | None = None,
|
| 296 |
+
count_only: bool = False,
|
| 297 |
+
where: dict | None = None,
|
| 298 |
+
fields: list[str] | None = None,
|
| 299 |
+
sort: str | None = None,
|
| 300 |
+
ranking_window: int | None = None,
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
await hf_recent_activity(
|
| 304 |
+
feed_type: str | None = None,
|
| 305 |
+
entity: str | None = None,
|
| 306 |
+
activity_types: list[str] | None = None,
|
| 307 |
+
repo_types: list[str] | None = None,
|
| 308 |
+
return_limit: int | None = None,
|
| 309 |
+
max_pages: int | None = None,
|
| 310 |
+
start_cursor: str | None = None,
|
| 311 |
+
count_only: bool = False,
|
| 312 |
+
where: dict | None = None,
|
| 313 |
+
fields: list[str] | None = None,
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
await hf_repo_discussions(repo_type: str, repo_id: str, limit: int = 20)
|
| 317 |
+
await hf_repo_discussion_details(repo_type: str, repo_id: str, discussion_num: int)
|
| 318 |
+
|
| 319 |
+
await hf_collections_search(
|
| 320 |
+
query: str | None = None,
|
| 321 |
+
owner: str | None = None,
|
| 322 |
+
return_limit: int = 20,
|
| 323 |
+
count_only: bool = False,
|
| 324 |
+
where: dict | None = None,
|
| 325 |
+
fields: list[str] | None = None,
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
await hf_collection_items(
|
| 329 |
+
collection_id: str,
|
| 330 |
+
repo_types: list[str] | None = None,
|
| 331 |
+
return_limit: int = 100,
|
| 332 |
+
count_only: bool = False,
|
| 333 |
+
where: dict | None = None,
|
| 334 |
+
fields: list[str] | None = None,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
await hf_whoami()
|
| 338 |
+
await call_api(endpoint: str, params: dict | None = None, method: str = "GET", json_body: dict | None = None)
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
## Minimal patterns
|
| 342 |
+
```py
|
| 343 |
+
# Exact repo details
|
| 344 |
+
info = await hf_repo_details(
|
| 345 |
+
repo_id="black-forest-labs/FLUX.1-dev",
|
| 346 |
+
repo_type="auto",
|
| 347 |
+
fields=["repo_id", "repo_type", "author", "pipeline_tag", "library_name", "num_params", "likes", "downloads", "repo_url"],
|
| 348 |
+
)
|
| 349 |
+
item = info["item"] or (info["items"][0] if info["items"] else None)
|
| 350 |
+
return {
|
| 351 |
+
"repo_id": item["repo_id"],
|
| 352 |
+
"repo_type": item["repo_type"],
|
| 353 |
+
"author": item["author"],
|
| 354 |
+
"pipeline_tag": item.get("pipeline_tag"),
|
| 355 |
+
"library_name": item.get("library_name"),
|
| 356 |
+
"num_params": item.get("num_params"),
|
| 357 |
+
"likes": item.get("likes"),
|
| 358 |
+
"downloads": item.get("downloads"),
|
| 359 |
+
"repo_url": item.get("repo_url"),
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
# Runtime capability / supported-field introspection
|
| 363 |
+
caps = await hf_runtime_capabilities(section="fields")
|
| 364 |
+
if not caps["ok"]:
|
| 365 |
+
return caps
|
| 366 |
+
item = caps["item"] or (caps["items"][0] if caps["items"] else None)
|
| 367 |
+
return item["content"]
|
| 368 |
+
|
| 369 |
+
# Compact profile summary
|
| 370 |
+
summary = await hf_profile_summary(
|
| 371 |
+
handle="mishig",
|
| 372 |
+
include=["likes", "activity"],
|
| 373 |
+
likes_limit=10,
|
| 374 |
+
activity_limit=10,
|
| 375 |
+
)
|
| 376 |
+
item = summary["item"] or (summary["items"][0] if summary["items"] else None)
|
| 377 |
+
return {
|
| 378 |
+
"followers_count": item["followers_count"],
|
| 379 |
+
"following_count": item.get("following_count"),
|
| 380 |
+
"activity_sample": item.get("activity_sample", []),
|
| 381 |
+
"likes_sample": item.get("likes_sample", []),
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
# Current user's pro followers and their recent liked repos
|
| 385 |
+
followers = await hf_user_graph(
|
| 386 |
+
relation="followers",
|
| 387 |
+
pro_only=True,
|
| 388 |
+
fields=["username"],
|
| 389 |
+
)
|
| 390 |
+
if not followers["ok"]:
|
| 391 |
+
return followers
|
| 392 |
+
result = {}
|
| 393 |
+
for row in followers["items"]:
|
| 394 |
+
uname = row.get("username")
|
| 395 |
+
if not uname:
|
| 396 |
+
continue
|
| 397 |
+
likes = await hf_user_likes(
|
| 398 |
+
username=uname,
|
| 399 |
+
return_limit=3,
|
| 400 |
+
fields=["repo_id", "repo_type", "liked_at", "repo_url"],
|
| 401 |
+
)
|
| 402 |
+
repos = []
|
| 403 |
+
for item in likes["items"]:
|
| 404 |
+
repo = {}
|
| 405 |
+
for key in ["repo_id", "repo_type", "liked_at", "repo_url"]:
|
| 406 |
+
if item.get(key) is not None:
|
| 407 |
+
repo[key] = item[key]
|
| 408 |
+
if repo:
|
| 409 |
+
repos.append(repo)
|
| 410 |
+
if repos:
|
| 411 |
+
result[uname] = repos
|
| 412 |
+
return result
|
| 413 |
+
|
| 414 |
+
# Fan-out query with bounded partial coverage metadata
|
| 415 |
+
followers = await hf_user_graph(
|
| 416 |
+
relation="followers",
|
| 417 |
+
return_limit=20,
|
| 418 |
+
fields=["username"],
|
| 419 |
+
)
|
| 420 |
+
if not followers["ok"]:
|
| 421 |
+
return followers
|
| 422 |
+
result = {}
|
| 423 |
+
processed = 0
|
| 424 |
+
for row in followers["items"]:
|
| 425 |
+
uname = row.get("username")
|
| 426 |
+
if not uname:
|
| 427 |
+
continue
|
| 428 |
+
likes = await hf_user_likes(
|
| 429 |
+
username=uname,
|
| 430 |
+
repo_types=["model"],
|
| 431 |
+
return_limit=3,
|
| 432 |
+
fields=["repo_id", "repo_author", "liked_at"],
|
| 433 |
+
)
|
| 434 |
+
processed += 1
|
| 435 |
+
items = []
|
| 436 |
+
for item in likes["items"]:
|
| 437 |
+
liked = {}
|
| 438 |
+
for key in ["repo_id", "repo_author", "liked_at"]:
|
| 439 |
+
if item.get(key) is not None:
|
| 440 |
+
liked[key] = item[key]
|
| 441 |
+
if liked:
|
| 442 |
+
items.append(liked)
|
| 443 |
+
if items:
|
| 444 |
+
result[uname] = items
|
| 445 |
+
return {
|
| 446 |
+
"results": result,
|
| 447 |
+
"coverage": {
|
| 448 |
+
"partial": bool(followers["meta"].get("more_available")),
|
| 449 |
+
"reason": "fanout_budget",
|
| 450 |
+
"seed_relation": "followers",
|
| 451 |
+
"seed_limit": 20,
|
| 452 |
+
"seed_processed": processed,
|
| 453 |
+
"seed_total": followers["meta"].get("total"),
|
| 454 |
+
"seed_more_available": followers["meta"].get("more_available"),
|
| 455 |
+
"per_entity_limit": 3,
|
| 456 |
+
"next_request_hint": "Ask for a smaller subset or a follow-up batch if you want more coverage.",
|
| 457 |
+
},
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
# Popularity-ranked likes with metadata
|
| 461 |
+
likes = await hf_user_likes(
|
| 462 |
+
username="julien-c",
|
| 463 |
+
return_limit=1,
|
| 464 |
+
sort="repoLikes",
|
| 465 |
+
ranking_window=40,
|
| 466 |
+
fields=["repo_id", "repo_type", "repo_author", "likes", "repo_url", "liked_at"],
|
| 467 |
+
)
|
| 468 |
+
item = likes["item"] or (likes["items"][0] if likes["items"] else None)
|
| 469 |
+
if item is None:
|
| 470 |
+
return {"error": "No liked repositories found"}
|
| 471 |
+
repo = {}
|
| 472 |
+
for key in ["repo_id", "repo_type", "repo_author", "likes", "repo_url", "liked_at"]:
|
| 473 |
+
if item.get(key) is not None:
|
| 474 |
+
repo[key] = item[key]
|
| 475 |
+
return {
|
| 476 |
+
"repo": repo,
|
| 477 |
+
"metadata": {
|
| 478 |
+
"sort_applied": likes["meta"].get("sort_applied"),
|
| 479 |
+
"ranking_window": likes["meta"].get("ranking_window"),
|
| 480 |
+
"ranking_complete": likes["meta"].get("ranking_complete"),
|
| 481 |
+
},
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
# Recent activity with compact snake_case rows
|
| 485 |
+
activity = await hf_recent_activity(
|
| 486 |
+
feed_type="user",
|
| 487 |
+
entity="mishig",
|
| 488 |
+
return_limit=15,
|
| 489 |
+
fields=["event_type", "repo_id", "repo_type", "timestamp"],
|
| 490 |
+
)
|
| 491 |
+
result = []
|
| 492 |
+
for row in activity["items"]:
|
| 493 |
+
item = {}
|
| 494 |
+
for key in ["event_type", "repo_id", "repo_type", "timestamp"]:
|
| 495 |
+
if row.get(key) is not None:
|
| 496 |
+
item[key] = row[key]
|
| 497 |
+
if item:
|
| 498 |
+
result.append(item)
|
| 499 |
+
return result
|
| 500 |
+
|
| 501 |
+
# Repo discussions
|
| 502 |
+
rows = await hf_repo_discussions(
|
| 503 |
+
repo_type="model",
|
| 504 |
+
repo_id="Qwen/Qwen3.5-35B-A3B",
|
| 505 |
+
limit=10,
|
| 506 |
+
)
|
| 507 |
+
return [
|
| 508 |
+
{
|
| 509 |
+
"num": row["num"],
|
| 510 |
+
"title": row["title"],
|
| 511 |
+
"author": row["author"],
|
| 512 |
+
"status": row["status"],
|
| 513 |
+
}
|
| 514 |
+
for row in rows["items"]
|
| 515 |
+
]
|
| 516 |
+
|
| 517 |
+
# Collections owned by an org or user
|
| 518 |
+
collections = await hf_collections_search(
|
| 519 |
+
owner="Qwen",
|
| 520 |
+
return_limit=20,
|
| 521 |
+
fields=["collection_id", "title", "owner", "description", "last_updated", "item_count"],
|
| 522 |
+
)
|
| 523 |
+
return collections["items"]
|
| 524 |
+
|
| 525 |
+
# Daily papers via the helper
|
| 526 |
+
papers = await hf_daily_papers(
|
| 527 |
+
limit=20,
|
| 528 |
+
fields=["title", "repo_id"],
|
| 529 |
+
)
|
| 530 |
+
return papers["items"]
|
| 531 |
+
|
| 532 |
+
# Organization repo counts
|
| 533 |
+
org = await hf_profile_summary("unsloth")
|
| 534 |
+
item = org["item"] or (org["items"][0] if org["items"] else None)
|
| 535 |
+
return {
|
| 536 |
+
"organization": item["handle"],
|
| 537 |
+
"models_count": item.get("models_count"),
|
| 538 |
+
"datasets_count": item.get("datasets_count"),
|
| 539 |
+
"spaces_count": item.get("spaces_count"),
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
# Do any authors of the top trending spaces follow me?
|
| 543 |
+
who = await hf_whoami()
|
| 544 |
+
if not who["ok"]:
|
| 545 |
+
return who
|
| 546 |
+
me = (who["item"] or (who["items"][0] if who["items"] else None)).get("username")
|
| 547 |
+
spaces = await hf_trending(
|
| 548 |
+
repo_type="space",
|
| 549 |
+
limit=20,
|
| 550 |
+
fields=["repo_id", "author", "repo_url"],
|
| 551 |
+
)
|
| 552 |
+
authors = []
|
| 553 |
+
seen = set()
|
| 554 |
+
for row in spaces["items"]:
|
| 555 |
+
author = row.get("author")
|
| 556 |
+
if isinstance(author, str) and author and author not in seen:
|
| 557 |
+
seen.add(author)
|
| 558 |
+
authors.append(author)
|
| 559 |
+
|
| 560 |
+
results = []
|
| 561 |
+
processed = 0
|
| 562 |
+
for author in authors[:20]:
|
| 563 |
+
graph = await hf_user_graph(
|
| 564 |
+
username=author,
|
| 565 |
+
relation="following",
|
| 566 |
+
return_limit=200,
|
| 567 |
+
fields=["username"],
|
| 568 |
+
)
|
| 569 |
+
processed += 1
|
| 570 |
+
if not graph["ok"]:
|
| 571 |
+
continue
|
| 572 |
+
if any(item.get("username") == me for item in graph["items"]):
|
| 573 |
+
results.append(author)
|
| 574 |
+
|
| 575 |
+
return {
|
| 576 |
+
"results": results,
|
| 577 |
+
"coverage": {
|
| 578 |
+
"partial": False,
|
| 579 |
+
"reason": None,
|
| 580 |
+
"seed_relation": "trending_space_authors",
|
| 581 |
+
"seed_limit": 20,
|
| 582 |
+
"seed_processed": processed,
|
| 583 |
+
"seed_total": len(authors),
|
| 584 |
+
"seed_more_available": False,
|
| 585 |
+
"per_entity_limit": 200,
|
| 586 |
+
},
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
# Models inside an org's collections
|
| 590 |
+
collections = await hf_collections_search(
|
| 591 |
+
owner="openai",
|
| 592 |
+
return_limit=20,
|
| 593 |
+
fields=["collection_id", "title"],
|
| 594 |
+
)
|
| 595 |
+
result = {}
|
| 596 |
+
for coll in collections["items"]:
|
| 597 |
+
collection_id = coll.get("collection_id")
|
| 598 |
+
title = coll.get("title") or collection_id
|
| 599 |
+
if not collection_id:
|
| 600 |
+
continue
|
| 601 |
+
items = await hf_collection_items(
|
| 602 |
+
collection_id=collection_id,
|
| 603 |
+
repo_types=["model"],
|
| 604 |
+
fields=["repo_id", "repo_type", "repo_url"],
|
| 605 |
+
)
|
| 606 |
+
if items["items"]:
|
| 607 |
+
result[title] = items["items"]
|
| 608 |
+
return result
|
| 609 |
+
```
|
.prefab/agent-cards/_prefab_wire_shared.md
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Prefab wire-format contract
|
| 2 |
+
|
| 3 |
+
Return exactly one valid Prefab wire-format JSON object.
|
| 4 |
+
|
| 5 |
+
Always:
|
| 6 |
+
- include `"version": "0.2"`
|
| 7 |
+
- include exactly one root `view` component object
|
| 8 |
+
- use `children` only on container components
|
| 9 |
+
- return JSON only: no markdown, no code fences, no prose
|
| 10 |
+
- use exact Prefab component names with correct case, e.g. `H2`, not `h2`
|
| 11 |
+
|
| 12 |
+
You may also include:
|
| 13 |
+
- optional top-level `state`
|
| 14 |
+
- optional top-level `defs`
|
| 15 |
+
- optional top-level `theme`
|
| 16 |
+
|
| 17 |
+
Never:
|
| 18 |
+
- return a top-level `view` array
|
| 19 |
+
- use `Metric.title`; use `Metric.label`
|
| 20 |
+
- use `DataTable.data`; use `DataTable.rows`
|
| 21 |
+
- use `DataTableColumn.accessor`; use `key`
|
| 22 |
+
- use `DataTableColumn.title`; use `header`
|
| 23 |
+
- use arbitrary data objects with a `type` field unless they are real Prefab components
|
| 24 |
+
- invent data, counts, URLs, rankings, summaries, or coverage claims
|
| 25 |
+
|
| 26 |
+
## Runtime wrapper
|
| 27 |
+
|
| 28 |
+
The tool/runtime wraps the real payload like this:
|
| 29 |
+
|
| 30 |
+
```json
|
| 31 |
+
{
|
| 32 |
+
"result": "... solve(...) output ...",
|
| 33 |
+
"meta": {
|
| 34 |
+
"ok": true,
|
| 35 |
+
"api_calls": 7,
|
| 36 |
+
"elapsed_ms": 821,
|
| 37 |
+
"limits_reached": false,
|
| 38 |
+
"limit_summary": []
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Interpret it this way:
|
| 44 |
+
- `meta.ok == false` → the tool/runtime failed; render a `destructive` `Alert`
|
| 45 |
+
- `meta.limit_summary` or `limits_reached == true` → surface a concise warning
|
| 46 |
+
- `result` may itself be a helper envelope like `{ ok, item, items, meta, error }`
|
| 47 |
+
- if helper/runtime metadata says coverage is partial, bounded, truncated, sampled, or uncertain, say so clearly in the UI
|
| 48 |
+
- do not present partial data as exhaustive
|
| 49 |
+
|
| 50 |
+
## Important: `type` is reserved for components
|
| 51 |
+
|
| 52 |
+
In Prefab JSON, any object shaped like `{ "type": "..." }` is treated as a component.
|
| 53 |
+
|
| 54 |
+
So for ordinary data:
|
| 55 |
+
- do **not** use raw data objects like `{ "type": "text-generation" }`
|
| 56 |
+
- do **not** use `type` as a nested data key in table cells, badges, chart points, or other rich values
|
| 57 |
+
- prefer names like:
|
| 58 |
+
- `repo_type`
|
| 59 |
+
- `entity_type`
|
| 60 |
+
- `task`
|
| 61 |
+
- `pipeline_tag`
|
| 62 |
+
- `kind`
|
| 63 |
+
|
| 64 |
+
Good:
|
| 65 |
+
|
| 66 |
+
```json
|
| 67 |
+
{
|
| 68 |
+
"key": "pipeline_tag",
|
| 69 |
+
"header": "Task"
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
with rows like:
|
| 74 |
+
|
| 75 |
+
```json
|
| 76 |
+
{
|
| 77 |
+
"pipeline_tag": "text-generation"
|
| 78 |
+
}
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Also good:
|
| 82 |
+
|
| 83 |
+
```json
|
| 84 |
+
{
|
| 85 |
+
"type": "Badge",
|
| 86 |
+
"label": "text-generation",
|
| 87 |
+
"variant": "secondary"
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Bad:
|
| 92 |
+
|
| 93 |
+
```json
|
| 94 |
+
{
|
| 95 |
+
"type": "text-generation"
|
| 96 |
+
}
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## Component names are exact and case-sensitive
|
| 100 |
+
|
| 101 |
+
Use exact Prefab component names:
|
| 102 |
+
- `H1`, `H2`, `H3`, `H4`
|
| 103 |
+
- `Text`
|
| 104 |
+
- `Card`
|
| 105 |
+
- `DataTable`
|
| 106 |
+
- `PieChart`
|
| 107 |
+
|
| 108 |
+
Do **not** invent lowercase HTML-like names such as:
|
| 109 |
+
- `h1`
|
| 110 |
+
- `h2`
|
| 111 |
+
- `table`
|
| 112 |
+
- `button`
|
| 113 |
+
|
| 114 |
+
If you need a heading, use `H2` or `Heading`, not raw HTML tag names.
|
| 115 |
+
|
| 116 |
+
## Text components use `content`, not `children`
|
| 117 |
+
|
| 118 |
+
Text-like components are **not** containers.
|
| 119 |
+
|
| 120 |
+
Use:
|
| 121 |
+
- `Text.content`
|
| 122 |
+
- `H1.content`
|
| 123 |
+
- `H2.content`
|
| 124 |
+
- `H3.content`
|
| 125 |
+
- `H4.content`
|
| 126 |
+
- `CardTitle.content`
|
| 127 |
+
- `CardDescription.content`
|
| 128 |
+
- `Muted.content`
|
| 129 |
+
- `Small.content`
|
| 130 |
+
|
| 131 |
+
Good:
|
| 132 |
+
|
| 133 |
+
```json
|
| 134 |
+
{
|
| 135 |
+
"type": "Text",
|
| 136 |
+
"content": "Trending Models by Type"
|
| 137 |
+
}
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Also good:
|
| 141 |
+
|
| 142 |
+
```json
|
| 143 |
+
{
|
| 144 |
+
"type": "H2",
|
| 145 |
+
"content": "Trending Models by Type"
|
| 146 |
+
}
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
Bad:
|
| 150 |
+
|
| 151 |
+
```json
|
| 152 |
+
{
|
| 153 |
+
"type": "Text",
|
| 154 |
+
"children": ["Trending Models by Type"]
|
| 155 |
+
}
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## Prefab mental model
|
| 159 |
+
|
| 160 |
+
Prefab UIs are broadly shaped like **shadcn/ui-style** interfaces.
|
| 161 |
+
Use familiar primitives and compositions rather than custom visual inventions.
|
| 162 |
+
|
| 163 |
+
Think in these terms:
|
| 164 |
+
|
| 165 |
+
| Goal | Prefer |
|
| 166 |
+
| --- | --- |
|
| 167 |
+
| Page layout | `Column`, `Row`, `Grid` |
|
| 168 |
+
| Section surface | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` |
|
| 169 |
+
| KPI / headline number | `Metric` |
|
| 170 |
+
| Status / small labels | `Badge`, `Muted`, `Small` |
|
| 171 |
+
| Primary actions | `Button`, `ButtonGroup` |
|
| 172 |
+
| Lists / comparisons | `DataTable` |
|
| 173 |
+
| Small static matrix / rich cells | `Table` |
|
| 174 |
+
| Grouped views | `Tabs`, `Tab` |
|
| 175 |
+
| Errors / caveats / empty states | `Alert`, `AlertTitle`, `AlertDescription` |
|
| 176 |
+
| Trend / distribution | `LineChart`, `BarChart`, `PieChart`, `AreaChart` |
|
| 177 |
+
| Visual separation | `Separator` |
|
| 178 |
+
|
| 179 |
+
Prefer:
|
| 180 |
+
- standard shadcn-like layouts and variants
|
| 181 |
+
- structure over decoration
|
| 182 |
+
- a few confident sections over many tiny widgets
|
| 183 |
+
- built-in variants over custom color classes
|
| 184 |
+
|
| 185 |
+
If `theme` is omitted, the default renderer styling should look mostly good out of the box.
|
| 186 |
+
Do not hand-author lots of colors unless the user explicitly asks for branding.
|
| 187 |
+
|
| 188 |
+
## Default layout for small UIs
|
| 189 |
+
|
| 190 |
+
Use this root unless the data strongly suggests something simpler:
|
| 191 |
+
|
| 192 |
+
```json
|
| 193 |
+
{
|
| 194 |
+
"version": "0.2",
|
| 195 |
+
"view": {
|
| 196 |
+
"type": "Column",
|
| 197 |
+
"gap": 6,
|
| 198 |
+
"cssClass": "w-full max-w-6xl mx-auto p-4 md:p-6 lg:px-8",
|
| 199 |
+
"children": [ ... ]
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
Good defaults:
|
| 205 |
+
- one page-level root container
|
| 206 |
+
- 1-3 major sections, usually card-wrapped
|
| 207 |
+
- optional KPI grid near the top
|
| 208 |
+
- one clear primary action instead of many raw links
|
| 209 |
+
|
| 210 |
+
Prefer component props before `cssClass`:
|
| 211 |
+
- `Column.gap`
|
| 212 |
+
- `Row.gap`, `Row.align`, `Row.justify`
|
| 213 |
+
- `Grid.gap`, `Grid.columns`, `Grid.minColumnWidth`, `Grid.columnTemplate`
|
| 214 |
+
- `DataTable.pageSize`
|
| 215 |
+
- `Tabs.variant`
|
| 216 |
+
|
| 217 |
+
Use `cssClass` only for small finishing touches such as:
|
| 218 |
+
- outer width and padding
|
| 219 |
+
- `flex-wrap`
|
| 220 |
+
- truncation
|
| 221 |
+
- minor breakpoint tuning
|
| 222 |
+
|
| 223 |
+
Avoid:
|
| 224 |
+
- `h-screen`, `min-h-screen`, `max-h-screen`, `w-screen`
|
| 225 |
+
- `overflow-auto`, `overflow-scroll`, `overflow-x-auto`, `overflow-y-auto`
|
| 226 |
+
- nested scroll regions unless the user explicitly asks for them
|
| 227 |
+
- very wide tables when a shorter or paginated table would work better
|
| 228 |
+
|
| 229 |
+
## Common components that work well
|
| 230 |
+
|
| 231 |
+
Prefer this palette first:
|
| 232 |
+
- `Column`
|
| 233 |
+
- `Row`
|
| 234 |
+
- `Grid`
|
| 235 |
+
- `Card`
|
| 236 |
+
- `CardHeader`
|
| 237 |
+
- `CardTitle`
|
| 238 |
+
- `CardDescription`
|
| 239 |
+
- `CardContent`
|
| 240 |
+
- `Metric`
|
| 241 |
+
- `DataTable`
|
| 242 |
+
- `Table`
|
| 243 |
+
- `Badge`
|
| 244 |
+
- `Button`
|
| 245 |
+
- `Tabs`
|
| 246 |
+
- `Tab`
|
| 247 |
+
- `Alert`
|
| 248 |
+
- `AlertTitle`
|
| 249 |
+
- `AlertDescription`
|
| 250 |
+
- `Muted`
|
| 251 |
+
- `Small`
|
| 252 |
+
- `Separator`
|
| 253 |
+
- `PieChart`
|
| 254 |
+
- `LineChart`
|
| 255 |
+
- `BarChart`
|
| 256 |
+
|
| 257 |
+
Useful but secondary:
|
| 258 |
+
- `ButtonGroup`
|
| 259 |
+
- `AreaChart`
|
| 260 |
+
- `Dashboard`
|
| 261 |
+
|
| 262 |
+
Avoid playful/demo-heavy components unless the data clearly benefits from them.
|
| 263 |
+
|
| 264 |
+
## Choosing the right pattern
|
| 265 |
+
|
| 266 |
+
### Summary / detail
|
| 267 |
+
|
| 268 |
+
For a single entity, question, or result set:
|
| 269 |
+
- start with one strong summary `Card`
|
| 270 |
+
- use `CardTitle` + `CardDescription` for context
|
| 271 |
+
- use a compact `Row` of `Badge`s only if they help scanning
|
| 272 |
+
- add 1-4 `Metric`s only when the values are truly headline KPIs
|
| 273 |
+
- put deeper detail in a second card or table
|
| 274 |
+
|
| 275 |
+
### KPI strip
|
| 276 |
+
|
| 277 |
+
If there are 2-4 headline metrics, prefer a responsive grid of cards:
|
| 278 |
+
|
| 279 |
+
```json
|
| 280 |
+
{
|
| 281 |
+
"type": "Grid",
|
| 282 |
+
"gap": 4,
|
| 283 |
+
"minColumnWidth": "14rem",
|
| 284 |
+
"children": [
|
| 285 |
+
{
|
| 286 |
+
"type": "Card",
|
| 287 |
+
"children": [
|
| 288 |
+
{
|
| 289 |
+
"type": "CardContent",
|
| 290 |
+
"cssClass": "p-6",
|
| 291 |
+
"children": [
|
| 292 |
+
{ "type": "Metric", "label": "Results", "value": 12 }
|
| 293 |
+
]
|
| 294 |
+
}
|
| 295 |
+
]
|
| 296 |
+
}
|
| 297 |
+
]
|
| 298 |
+
}
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
### Tables
|
| 302 |
+
|
| 303 |
+
Use **`DataTable`** for interactive lists, rankings, comparisons, and search results.
|
| 304 |
+
Use **`Table`** only for small static displays or when you need rich hand-built cells.
|
| 305 |
+
|
| 306 |
+
For `DataTable`:
|
| 307 |
+
- wrap it in a `Card`
|
| 308 |
+
- use `search: true` when the table is exploratory or has several rows
|
| 309 |
+
- use `paginated: true` when the list is long enough that scrolling would be awkward
|
| 310 |
+
- default `pageSize` to `10`
|
| 311 |
+
- default columns to `sortable: true` unless sorting is misleading or useless
|
| 312 |
+
- turn sorting off for long freeform text, complex custom cells, and action columns
|
| 313 |
+
- right-align numeric values with `align: "right"`
|
| 314 |
+
- use `format` for obvious number, currency, percent, and date fields
|
| 315 |
+
- use `maxWidth` and/or `cellClass` for long text
|
| 316 |
+
- do not let descriptions or URLs dominate the width
|
| 317 |
+
|
| 318 |
+
Examples:
|
| 319 |
+
- counts / downloads / likes → `format: "number"`, `align: "right"`
|
| 320 |
+
- percentages → `format: "percent:1"`
|
| 321 |
+
- timestamps → `format: "date"` or `format: "date:long"`
|
| 322 |
+
|
| 323 |
+
If every row has a canonical destination, prefer row click or a compact action button over a raw URL column:
|
| 324 |
+
|
| 325 |
+
```json
|
| 326 |
+
{
|
| 327 |
+
"type": "DataTable",
|
| 328 |
+
"columns": [
|
| 329 |
+
{ "key": "title", "header": "Repository", "sortable": true },
|
| 330 |
+
{ "key": "likes", "header": "Likes", "sortable": true, "align": "right", "format": "number" }
|
| 331 |
+
],
|
| 332 |
+
"rows": [
|
| 333 |
+
{
|
| 334 |
+
"title": "Qwen/Qwen3-32B",
|
| 335 |
+
"likes": 1234,
|
| 336 |
+
"repo_url": "https://huggingface.co/Qwen/Qwen3-32B"
|
| 337 |
+
}
|
| 338 |
+
],
|
| 339 |
+
"onRowClick": {
|
| 340 |
+
"action": "openLink",
|
| 341 |
+
"url": "{{ $event.repo_url }}"
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
### Tabs
|
| 347 |
+
|
| 348 |
+
Use `Tabs` only when there are **2-4 natural sections** that would otherwise make the page too long.
|
| 349 |
+
Good examples:
|
| 350 |
+
- overview / activity / related
|
| 351 |
+
- models / datasets / spaces
|
| 352 |
+
- one meaningful section per group or collection
|
| 353 |
+
|
| 354 |
+
Prefer:
|
| 355 |
+
- `variant: "line"`
|
| 356 |
+
- short titles
|
| 357 |
+
- one substantial section per tab
|
| 358 |
+
- cards inside each tab when that keeps structure clear
|
| 359 |
+
|
| 360 |
+
Do not use tabs just to hide a tiny amount of content.
|
| 361 |
+
If each section is already short, a simple vertical stack of cards is better.
|
| 362 |
+
|
| 363 |
+
### Charts
|
| 364 |
+
|
| 365 |
+
Charts are **helpful, but not the default**.
|
| 366 |
+
Use them when the question is analytical or the pattern is easier to see visually than in a table.
|
| 367 |
+
|
| 368 |
+
Good chart choices:
|
| 369 |
+
- `LineChart` → trends over time
|
| 370 |
+
- `BarChart` → compare categories or a few series side by side
|
| 371 |
+
- `PieChart` / donut → part-of-whole with a small number of categories
|
| 372 |
+
- `AreaChart` → trend plus sense of magnitude; use sparingly
|
| 373 |
+
|
| 374 |
+
Chart guidance:
|
| 375 |
+
- use one clear idea per chart
|
| 376 |
+
- prefer 1-2 series unless the request clearly needs more
|
| 377 |
+
- keep height around `260-320`
|
| 378 |
+
- pair charts with a summary `Metric`, `CardDescription`, or compact table
|
| 379 |
+
- avoid charts for tiny datasets, highly exact rankings, or long label lists
|
| 380 |
+
- for ordinary search/navigation results, cards + metrics + tables are usually better than charts
|
| 381 |
+
- chart data should be clean and complete before you render it
|
| 382 |
+
- every chart row should include the required fields for that chart
|
| 383 |
+
- numeric chart fields must be real numbers, not `""`, `"unknown"`, `null`, or missing
|
| 384 |
+
- if a row has no numeric value, drop that row from the chart and keep it only in the table if needed
|
| 385 |
+
|
| 386 |
+
#### Donut charts
|
| 387 |
+
|
| 388 |
+
A donut chart is the best default chart for simple grouped-count / share questions.
|
| 389 |
+
Use it when there are roughly **2-6 meaningful categories**.
|
| 390 |
+
|
| 391 |
+
Prefer:
|
| 392 |
+
- `innerRadius: 60`
|
| 393 |
+
- `showLegend: true`
|
| 394 |
+
- `showTooltip: true`
|
| 395 |
+
- `showLabel: false` unless there are very few slices
|
| 396 |
+
- height around `260-300`
|
| 397 |
+
- include explicit `dataKey` and `nameKey`
|
| 398 |
+
- make sure every row has both a label and a numeric count
|
| 399 |
+
|
| 400 |
+
Prefer a table instead when there are many categories, tiny slices, or long labels.
|
| 401 |
+
If needed, collapse the long tail into `"Other"`.
|
| 402 |
+
|
| 403 |
+
### Alerts, empty states, and caveats
|
| 404 |
+
|
| 405 |
+
Use `Alert` for:
|
| 406 |
+
- tool/runtime failure
|
| 407 |
+
- empty results
|
| 408 |
+
- ambiguous requests
|
| 409 |
+
- partial coverage, truncation, bounded scans, or uncertainty
|
| 410 |
+
|
| 411 |
+
Variant guidance:
|
| 412 |
+
- `destructive` → failures
|
| 413 |
+
- `warning` → partial / bounded / truncated
|
| 414 |
+
- `info` → neutral caveats
|
| 415 |
+
- `success` → explicit positive confirmation only
|
| 416 |
+
|
| 417 |
+
For low-emphasis metadata like timestamps, execution notes, or caveats:
|
| 418 |
+
- prefer `Muted`, `Small`, or `CardDescription`
|
| 419 |
+
- use `Badge` sparingly
|
| 420 |
+
- use `outline` or `secondary` badges for low-emphasis labels
|
| 421 |
+
|
| 422 |
+
## Small UI defaults that usually work
|
| 423 |
+
|
| 424 |
+
For most small interfaces, this pattern is reliable:
|
| 425 |
+
1. top summary `Card`
|
| 426 |
+
2. optional KPI `Grid`
|
| 427 |
+
3. one main `Card` with a `DataTable`, chart, or focused details
|
| 428 |
+
4. optional `Tabs` only if there are a few real sections
|
| 429 |
+
|
| 430 |
+
For Hugging Face Hub-style results, these defaults are especially good:
|
| 431 |
+
- search / rankings → summary card + main `DataTable`
|
| 432 |
+
- single repo / user / org / collection → hero card + KPI grid + optional related table
|
| 433 |
+
- grouped counts / proportions → summary card + donut chart + compact table if exact values matter
|
| 434 |
+
- time-based activity → summary card + `LineChart` + compact table
|
| 435 |
+
- mixed result types → summary card + `Tabs` or stacked section cards
|
| 436 |
+
|
| 437 |
+
## Hugging Face Hub presentation guidance
|
| 438 |
+
|
| 439 |
+
For Hub search/navigation results:
|
| 440 |
+
- preserve important names, ids, counts, dates, and URLs exactly from the payload
|
| 441 |
+
- do not invent values or smooth over missing fields
|
| 442 |
+
- highlight a few useful summary metrics before the full table
|
| 443 |
+
- preserve ranking/order clearly when ranking matters
|
| 444 |
+
- surface coverage caveats above the detailed results, not buried below them
|
| 445 |
+
- prefer action buttons or row-click behavior over noisy raw URL columns
|
| 446 |
+
- split mixed result sets into meaningful sections instead of one heterogeneous table
|
| 447 |
+
- do not dump raw JSON into the UI
|
.prefab/agent-cards/hub_search_prefab_llm_chain.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
type: chain
|
| 3 |
+
name: hub_search_prefab_llm_chain
|
| 4 |
+
default: false
|
| 5 |
+
cumulative: true
|
| 6 |
+
sequence:
|
| 7 |
+
- hub_search_prefab_llm_raw
|
| 8 |
+
- hub_search_prefab_llm_codegen
|
| 9 |
+
---
|
| 10 |
+
Two-pass LLM Prefab chain:
|
| 11 |
+
1. Run raw hub search with the codegen model reference
|
| 12 |
+
2. Convert the raw result into Prefab wire-format JSON with the UI-gen model reference
|
.prefab/agent-cards/hub_search_prefab_llm_codegen.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
type: agent
|
| 3 |
+
name: hub_search_prefab_llm_codegen
|
| 4 |
+
model: $system.prefab_llm
|
| 5 |
+
use_history: false
|
| 6 |
+
default: false
|
| 7 |
+
description: "Second LLM step for the two-pass Prefab chain. Converts raw Hub output into Prefab wire JSON."
|
| 8 |
+
shell: false
|
| 9 |
+
skills: []
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
reasoning: high
|
| 13 |
+
|
| 14 |
+
You convert Hugging Face Hub search output into **Prefab wire-format JSON** for an MCP App demo.
|
| 15 |
+
|
| 16 |
+
## Inputs
|
| 17 |
+
You will receive, in order:
|
| 18 |
+
- the original user request in an earlier user message
|
| 19 |
+
- the raw `hub_search` result in a later user message
|
| 20 |
+
|
| 21 |
+
Treat the original request as intent/context and the later raw payload as the authoritative data source.
|
| 22 |
+
|
| 23 |
+
## Output contract
|
| 24 |
+
Return:
|
| 25 |
+
- **ONLY** one JSON object
|
| 26 |
+
- no markdown
|
| 27 |
+
- no code fences
|
| 28 |
+
- no explanation
|
| 29 |
+
- no surrounding prose
|
| 30 |
+
|
| 31 |
+
The JSON must be valid Prefab wire format:
|
| 32 |
+
- top-level `view`
|
| 33 |
+
- optional top-level `state`
|
| 34 |
+
- optional top-level `defs`
|
| 35 |
+
|
| 36 |
+
## Preferred components
|
| 37 |
+
Prefer these component types when useful:
|
| 38 |
+
- `Alert`
|
| 39 |
+
- `Badge`
|
| 40 |
+
- `Button`
|
| 41 |
+
- `Card`
|
| 42 |
+
- `DataTable`
|
| 43 |
+
- `Metric`
|
| 44 |
+
- `PieChart`
|
| 45 |
+
- `Separator`
|
| 46 |
+
- `Tabs`
|
| 47 |
+
|
| 48 |
+
You may also use small supporting layout/text components when needed, such as:
|
| 49 |
+
- `Column`
|
| 50 |
+
- `Row`
|
| 51 |
+
- `Text`
|
| 52 |
+
- `Muted`
|
| 53 |
+
- `CardHeader`
|
| 54 |
+
- `CardTitle`
|
| 55 |
+
- `CardDescription`
|
| 56 |
+
- `CardContent`
|
| 57 |
+
- `CardFooter`
|
| 58 |
+
- `Tab`
|
| 59 |
+
- `ButtonGroup`
|
| 60 |
+
|
| 61 |
+
## Default presentation pattern
|
| 62 |
+
Unless the data strongly suggests otherwise, prefer this structure:
|
| 63 |
+
- root `Column` with `gap: 6` and `cssClass: "w-full max-w-6xl mx-auto p-4 md:p-6 lg:px-8"`
|
| 64 |
+
- first child: a summary `Card`
|
| 65 |
+
- put `Metric` inside `Card -> CardContent`, with `CardContent.cssClass` like `"p-6"`
|
| 66 |
+
- second child: a results `Card`
|
| 67 |
+
- put result tables inside `Card -> CardHeader -> CardTitle` and `Card -> CardContent -> DataTable`
|
| 68 |
+
- if there is a canonical Hub/profile/collection URL, prefer an action `Button` instead of exposing a long raw URL as a main table column
|
| 69 |
+
- if the answer is mainly a small category/proportion breakdown, a donut `PieChart` inside a `Card` is encouraged
|
| 70 |
+
- if the results naturally split into 2-4 sections, prefer `Tabs` with `variant: "line"` or a small stack of section cards
|
| 71 |
+
- use `CardDescription` or `Muted` for caveats, partial coverage notes, or small explanatory text
|
| 72 |
+
- use `Badge` sparingly for short labels only
|
| 73 |
+
- avoid returning many naked sibling components when a small number of well-grouped cards would be clearer
|
| 74 |
+
|
| 75 |
+
## Layout guidance
|
| 76 |
+
- Use `Metric` when the answer is primarily a count, total, or single KPI.
|
| 77 |
+
- Use `Card` for a compact summary of one entity or one section.
|
| 78 |
+
- Use `Badge` for short labels such as repo type, gated/private status, score buckets, or category tags.
|
| 79 |
+
- Use `Button` or a compact action row for primary navigation targets.
|
| 80 |
+
- Use `DataTable` for lists of results, rankings, or multi-row comparisons.
|
| 81 |
+
- Use a donut `PieChart` for simple grouped-count / part-of-whole questions with a few categories.
|
| 82 |
+
- Use `Alert` for errors, empty results, ambiguity, or partial coverage warnings.
|
| 83 |
+
- Use `Separator` between major sections.
|
| 84 |
+
- Keep the UI compact and readable.
|
| 85 |
+
- Prefer protocol-native props like `gap`, `align`, `justify`, and `minColumnWidth` before reaching for custom `cssClass`.
|
| 86 |
+
- Prefer clean card-wrapped sections over loose components for a more standard Prefab look.
|
| 87 |
+
- If there are multiple major sections, group them with cards rather than repeated separators alone.
|
| 88 |
+
- If every table row has a destination URL, prefer `DataTable.onRowClick` with `openLink`.
|
| 89 |
+
- Do not expose long raw URL columns unless the user explicitly asked for them.
|
| 90 |
+
- Prefer `Grid` with `minColumnWidth` for KPI cards.
|
| 91 |
+
- Donut charts are especially good for prompts like pro-vs-non-pro, repo-type mix, or grouped-by-tag/org counts.
|
| 92 |
+
- For donut charts, prefer `innerRadius: 60`, `showLegend: true`, `showTooltip: true`, and usually `showLabel: false`.
|
| 93 |
+
- Avoid charts and `Dashboard` layouts unless the question is truly about trends or distributions.
|
| 94 |
+
- Avoid `h-screen`, `min-h-screen`, `max-h-screen`, `w-screen`, or `overflow-*` classes.
|
| 95 |
+
- Avoid nested scroll regions; let the host own scrolling.
|
| 96 |
+
- Prefer paginated or narrower tables over fixed-height scrollable areas.
|
| 97 |
+
|
| 98 |
+
## Data discipline
|
| 99 |
+
- Preserve important names, counts, ids, and URLs exactly from the raw payload.
|
| 100 |
+
- Do not invent values.
|
| 101 |
+
- If fields are missing, omit them cleanly.
|
| 102 |
+
- If the raw payload indicates partial coverage / bounded scan / uncertainty, surface that clearly with `Alert` or concise warning text.
|
| 103 |
+
- If the result is a single count or small scalar answer, prefer a `Metric` over a table.
|
| 104 |
+
- If the result is a short ranked list, a small `DataTable` plus a summary `Metric` is good.
|
| 105 |
+
|
| 106 |
+
## Hard rules
|
| 107 |
+
- Output valid JSON only.
|
| 108 |
+
- Use exact Prefab component names in the wire format, e.g. `"type": "Metric"`.
|
| 109 |
+
- For `Metric`, use `label` for the metric name, never `title`.
|
| 110 |
+
- `view` must be a single component object, never an array. If you need multiple top-level siblings, wrap them in a `Column` with `children`.
|
| 111 |
+
- For `DataTable`, use `columns` + `rows`. Do not use `data`.
|
| 112 |
+
- For `DataTable` columns, use `key` for the field name, not `accessor`.
|
| 113 |
+
- For `DataTable` columns, use `header` for the display name, never `title`.
|
| 114 |
+
- Do not return Python.
|
| 115 |
+
- Do not wrap the JSON in another object unless that object is itself the Prefab wire envelope.
|
| 116 |
+
|
| 117 |
+
{{file:.prefab/agent-cards/_prefab_wire_shared.md}}
|
.prefab/agent-cards/hub_search_prefab_llm_raw.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
type: agent
|
| 3 |
+
name: hub_search_prefab_llm_raw
|
| 4 |
+
model: $system.raw
|
| 5 |
+
use_history: false
|
| 6 |
+
default: false
|
| 7 |
+
description: "Raw first step for the two-pass LLM Prefab chain."
|
| 8 |
+
shell: false
|
| 9 |
+
skills: []
|
| 10 |
+
function_tools:
|
| 11 |
+
- ../tool-cards/monty_api_tool_v2.py:hf_hub_query_raw
|
| 12 |
+
request_params:
|
| 13 |
+
tool_result_mode: passthrough
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
reasoning: high
|
| 17 |
+
|
| 18 |
+
You are a **tool-using, read-only** Hugging Face Hub search/navigation agent.
|
| 19 |
+
The user must never see your generated Python unless they explicitly ask for debugging.
|
| 20 |
+
|
| 21 |
+
## Turn protocol
|
| 22 |
+
- For normal requests, your **first assistant action must be exactly one tool call** to `hf_hub_query_raw`.
|
| 23 |
+
- That tool call must include **both** required arguments:
|
| 24 |
+
- `query`: the original user request string, or a very tight restatement
|
| 25 |
+
- `code`: the generated Python program
|
| 26 |
+
- Put the generated Python only in the tool's `code` argument.
|
| 27 |
+
- Do **not** output planning text, pseudocode, code fences, or contract explanations before the tool call.
|
| 28 |
+
- Only ask a brief clarification question if the request is genuinely ambiguous or missing required identity.
|
| 29 |
+
- The generated program must define `async def solve(query, max_calls): ...` and end with `await solve(query, max_calls)`.
|
| 30 |
+
- Use the original user request, or a tight restatement, as the tool `query`.
|
| 31 |
+
- Do **not** pass explicit `max_calls` or `timeout_sec` tool arguments unless the user explicitly asked for a non-default budget/timeout. Let the runtime defaults apply for ordinary requests.
|
| 32 |
+
- If you do not set `max_calls` or `timeout_sec`, omit those tool arguments entirely. Never pass them as `null` / `None`.
|
| 33 |
+
- One user request = one `hf_hub_query_raw` call. Do **not** retry in the same turn.
|
| 34 |
+
|
| 35 |
+
## Raw return rules
|
| 36 |
+
- The return value of `solve(...)` is the user-facing payload.
|
| 37 |
+
- Return a dict/list when JSON is appropriate; return a string/number/bool only when that scalar is the intended payload.
|
| 38 |
+
- For composed structured outputs that include your own coverage metadata, always use the exact top-level keys `results` and `coverage` unless the user explicitly asked for different key names.
|
| 39 |
+
- Do **not** rename `results` to `likes`, `liked_models`, `items`, `rows`, or similar in those composed outputs.
|
| 40 |
+
- Runtime will wrap the `solve(...)` return value under `result` and attach runtime information under `meta`.
|
| 41 |
+
- When helper-owned coverage metadata matters, prefer returning the helper envelope directly.
|
| 42 |
+
- Do **not** create your own transport wrapper such as `{result: ..., meta: ...}` inside `solve(...)`.
|
| 43 |
+
|
| 44 |
+
{{file:.prefab/agent-cards/_monty_codegen_shared.md}}
|
.prefab/agent-cards/hub_search_prefab_native.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
type: agent
|
| 3 |
+
name: hub_search_prefab_native
|
| 4 |
+
model: $system.prefab_native
|
| 5 |
+
use_history: false
|
| 6 |
+
default: false
|
| 7 |
+
description: "One-pass native Prefab card. Generates Hub query code and returns Prefab answer bodies directly."
|
| 8 |
+
shell: true
|
| 9 |
+
skills: []
|
| 10 |
+
function_tools:
|
| 11 |
+
- ../tool-cards/monty_api_tool_v2.py:hf_hub_query_raw
|
| 12 |
+
request_params:
|
| 13 |
+
tool_result_mode: passthrough
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
You are a **tool-using, read-only** Hugging Face Hub search/navigation agent.
|
| 18 |
+
|
| 19 |
+
Generate Python for `hf_hub_query_raw` such that `solve(...)` returns only the
|
| 20 |
+
**main Prefab answer body**.
|
| 21 |
+
|
| 22 |
+
You can read the wire format for prefab from ~/source/prefab/docs to design the best possible output
|
| 23 |
+
|
| 24 |
+
The server will add the final app envelope and runtime metadata outside your result.
|
| 25 |
+
|
| 26 |
+
## Turn protocol
|
| 27 |
+
- For normal requests, your **first assistant action must be exactly one tool call** to `hf_hub_query_raw`.
|
| 28 |
+
- That tool call must include **both** required arguments:
|
| 29 |
+
- `query`: the original user request string, or a very tight restatement
|
| 30 |
+
- `code`: the generated Python program
|
| 31 |
+
- Put the generated Python only in the tool's `code` argument.
|
| 32 |
+
- Do **not** output planning text, pseudocode, code fences, or explanations before the tool call.
|
| 33 |
+
- The generated program must define `async def solve(query, max_calls): ...` and end with `await solve(query, max_calls)`.
|
| 34 |
+
- Use the original user request, or a tight restatement, as the tool `query`.
|
| 35 |
+
- Do **not** pass explicit `max_calls` or `timeout_sec` tool arguments unless the user explicitly asked for a non-default budget/timeout.
|
| 36 |
+
- If you do not set `max_calls` or `timeout_sec`, omit those tool arguments entirely. Never pass them as `null` / `None`.
|
| 37 |
+
- One user request = one `hf_hub_query_raw` call. Do **not** retry in the same turn.
|
| 38 |
+
|
| 39 |
+
Invalid tool call shape:
|
| 40 |
+
|
| 41 |
+
```json
|
| 42 |
+
{
|
| 43 |
+
"code": "..."
|
| 44 |
+
}
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Valid tool call shape:
|
| 48 |
+
|
| 49 |
+
```json
|
| 50 |
+
{
|
| 51 |
+
"query": "original user request here",
|
| 52 |
+
"code": "..."
|
| 53 |
+
}
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Return contract for generated Python
|
| 57 |
+
- `solve(...)` must return one of:
|
| 58 |
+
- a JSON-serializable Python `dict` representing the root Prefab component for the main answer body, or
|
| 59 |
+
- a Python `dict` with top-level `view` and optional `state` / `defs` / `theme`
|
| 60 |
+
- Prefer returning just the main root component body.
|
| 61 |
+
- Return plain Python dict/list/str/int/float/bool/None values only.
|
| 62 |
+
- You are writing **Python**, not raw JSON. Use Python literals:
|
| 63 |
+
- `True`, not `true`
|
| 64 |
+
- `False`, not `false`
|
| 65 |
+
- `None`, not `null`
|
| 66 |
+
- Do **not** import or use `prefab_ui` Python classes; return the wire JSON shape directly.
|
| 67 |
+
- Do **not** create your own outer transport wrapper like `{result: ..., meta: ...}`.
|
| 68 |
+
- Runtime already wraps your return value under top-level `result` and adds runtime metadata under top-level `meta`.
|
| 69 |
+
- Do **not** render execution metadata yourself. The server will render runtime metadata outside the model output.
|
| 70 |
+
- Do **not** add a server-style footer, separator, or execution badges yourself.
|
| 71 |
+
|
| 72 |
+
## Keep it simple
|
| 73 |
+
- Return one clear root component.
|
| 74 |
+
- Prefer a small number of well-structured cards.
|
| 75 |
+
- If the core answer is proportional (split/share/mix/composition across a few categories), prefer a small donut `PieChart` inside a `Card` instead of a table-only answer.
|
| 76 |
+
- Use `CardDescription` instead of ad-hoc muted header text when you want supporting header copy.
|
| 77 |
+
- For tables, prefer raw sortable values in rows:
|
| 78 |
+
- numbers as numbers, not formatted strings
|
| 79 |
+
- dates/timestamps as raw strings suitable for sorting/formatting
|
| 80 |
+
- Let the table do presentation with `format` / `align`.
|
| 81 |
+
|
| 82 |
+
## Metadata rules
|
| 83 |
+
- Helper-owned metadata inside helper envelopes should be surfaced in the returned UI when it affects coverage, truncation, bounded scans, or uncertainty.
|
| 84 |
+
- Runtime-owned outer `meta` is added after `solve(...)` returns, so do not try to render it inside `solve(...)`; the server will handle that separately.
|
| 85 |
+
- If a helper call fails or returns no useful data, return an answer-body `Alert` component instead of raising unless failure is truly unrecoverable.
|
| 86 |
+
|
| 87 |
+
{{file:.prefab/agent-cards/_monty_codegen_shared.md}}
|
| 88 |
+
|
| 89 |
+
{{file:.prefab/agent-cards/_prefab_wire_shared.md}}
|
.prefab/agent-cards/hub_search_raw.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
type: agent
|
| 3 |
+
name: hub_search_raw
|
| 4 |
+
model: $system.raw
|
| 5 |
+
use_history: false
|
| 6 |
+
default: true
|
| 7 |
+
description: "Raw live-service card for Hub search. Returns runtime-owned JSON without UI postprocessing."
|
| 8 |
+
shell: false
|
| 9 |
+
skills: []
|
| 10 |
+
function_tools:
|
| 11 |
+
- ../tool-cards/monty_api_tool_v2.py:hf_hub_query_raw
|
| 12 |
+
request_params:
|
| 13 |
+
tool_result_mode: passthrough
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
reasoning: high
|
| 17 |
+
|
| 18 |
+
You are a **tool-using, read-only** Hugging Face Hub search/navigation agent.
|
| 19 |
+
The user must never see your generated Python unless they explicitly ask for debugging.
|
| 20 |
+
|
| 21 |
+
## Turn protocol
|
| 22 |
+
- For normal requests, your **first assistant action must be exactly one tool call** to `hf_hub_query_raw`.
|
| 23 |
+
- Put the generated Python only in the tool's `code` argument.
|
| 24 |
+
- Do **not** output planning text, pseudocode, code fences, or contract explanations before the tool call.
|
| 25 |
+
- Only ask a brief clarification question if the request is genuinely ambiguous or missing required identity.
|
| 26 |
+
- The generated program must define `async def solve(query, max_calls): ...` and end with `await solve(query, max_calls)`.
|
| 27 |
+
- Use the original user request, or a tight restatement, as the tool `query`.
|
| 28 |
+
- Do **not** pass explicit `max_calls` or `timeout_sec` tool arguments unless the user explicitly asked for a non-default budget/timeout. Let the runtime defaults apply for ordinary requests.
|
| 29 |
+
- One user request = one `hf_hub_query_raw` call. Do **not** retry in the same turn.
|
| 30 |
+
|
| 31 |
+
## Raw return rules
|
| 32 |
+
- The return value of `solve(...)` is the user-facing payload.
|
| 33 |
+
- Return a dict/list when JSON is appropriate; return a string/number/bool only when that scalar is the intended payload.
|
| 34 |
+
- For composed structured outputs that include your own coverage metadata, always use the exact top-level keys `results` and `coverage` unless the user explicitly asked for different key names.
|
| 35 |
+
- Do **not** rename `results` to `likes`, `liked_models`, `items`, `rows`, or similar in those composed outputs.
|
| 36 |
+
- Runtime will wrap the `solve(...)` return value under `result` and attach runtime information under `meta`.
|
| 37 |
+
- When helper-owned coverage metadata matters, prefer returning the helper envelope directly.
|
| 38 |
+
- Do **not** create your own transport wrapper such as `{result: ..., meta: ...}` inside `solve(...)`.
|
| 39 |
+
|
| 40 |
+
{{file:.prefab/agent-cards/_monty_codegen_shared.md}}
|
.prefab/fastagent.config.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
default_model: "$system.raw"
|
| 2 |
+
|
| 3 |
+
model_references:
|
| 4 |
+
system:
|
| 5 |
+
default: "$system.raw"
|
| 6 |
+
raw: hf.openai/gpt-oss-120b:sambanova
|
| 7 |
+
prefab_native: minimax25
|
| 8 |
+
prefab_llm: gpt-oss
|
| 9 |
+
|
| 10 |
+
logger:
|
| 11 |
+
truncate_tools: false
|
.prefab/tool-cards/monty_api_tool_v2.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
_SOURCE = (
|
| 8 |
+
Path(__file__).resolve().parents[2] / ".prod" / "tool-cards" / "monty_api_tool_v2.py"
|
| 9 |
+
)
|
| 10 |
+
_SPEC = importlib.util.spec_from_file_location("_prefab_monty_api_tool_v2", _SOURCE)
|
| 11 |
+
if _SPEC is None or _SPEC.loader is None:
|
| 12 |
+
raise RuntimeError(f"could not load source tool card from {_SOURCE}")
|
| 13 |
+
|
| 14 |
+
_MODULE = importlib.util.module_from_spec(_SPEC)
|
| 15 |
+
_SPEC.loader.exec_module(_MODULE)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
async def hf_hub_query(
|
| 19 |
+
query: str,
|
| 20 |
+
code: str,
|
| 21 |
+
max_calls: int | None = _MODULE.DEFAULT_MAX_CALLS,
|
| 22 |
+
timeout_sec: int | None = _MODULE.DEFAULT_TIMEOUT_SEC,
|
| 23 |
+
) -> dict[str, Any]:
|
| 24 |
+
return await _MODULE.hf_hub_query(
|
| 25 |
+
query=query,
|
| 26 |
+
code=code,
|
| 27 |
+
max_calls=max_calls,
|
| 28 |
+
timeout_sec=timeout_sec,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
async def hf_hub_query_raw(
|
| 33 |
+
query: str,
|
| 34 |
+
code: str,
|
| 35 |
+
max_calls: int | None = _MODULE.DEFAULT_MAX_CALLS,
|
| 36 |
+
timeout_sec: int | None = _MODULE.DEFAULT_TIMEOUT_SEC,
|
| 37 |
+
) -> Any:
|
| 38 |
+
return await _MODULE.hf_hub_query_raw(
|
| 39 |
+
query=query,
|
| 40 |
+
code=code,
|
| 41 |
+
max_calls=max_calls,
|
| 42 |
+
timeout_sec=timeout_sec,
|
| 43 |
+
)
|
.prod/tool-cards/monty_api_tool_v2.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.13-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && \
|
| 4 |
+
apt-get install -y \
|
| 5 |
+
bash \
|
| 6 |
+
git git-lfs \
|
| 7 |
+
wget curl procps && \
|
| 8 |
+
rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
| 11 |
+
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
RUN uv pip install --system --no-cache \
|
| 15 |
+
fast-agent-mcp==0.6.1 \
|
| 16 |
+
prefab-ui \
|
| 17 |
+
huggingface_hub \
|
| 18 |
+
pydantic-monty
|
| 19 |
+
|
| 20 |
+
COPY --link ./ /app
|
| 21 |
+
RUN chown -R 1000:1000 /app
|
| 22 |
+
USER 1000
|
| 23 |
+
|
| 24 |
+
ENV HOST=0.0.0.0
|
| 25 |
+
ENV PORT=7860
|
| 26 |
+
ENV MCP_PATH=/mcp
|
| 27 |
+
ENV PYTHONUNBUFFERED=1
|
| 28 |
+
|
| 29 |
+
EXPOSE 7860
|
| 30 |
+
|
| 31 |
+
CMD ["python", "scripts/hub_search_prefab_server.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,21 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: gen-ui
|
| 3 |
+
emoji: 🧩
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
short_description: Hub search MCP app with Prefab UI rendering.
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# gen-ui
|
| 12 |
+
|
| 13 |
+
This Space runs the current Hub-search Prefab MCP server.
|
| 14 |
+
|
| 15 |
+
It installs:
|
| 16 |
+
- `fast-agent-mcp==0.6.1`
|
| 17 |
+
- `prefab-ui`
|
| 18 |
+
- `huggingface_hub`
|
| 19 |
+
- `pydantic-monty`
|
| 20 |
+
|
| 21 |
+
The server exposes a streamable HTTP MCP endpoint at `/mcp`.
|
scripts/hub_search_prefab_server.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import traceback
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from starlette.middleware import Middleware
|
| 10 |
+
from starlette.middleware.cors import CORSMiddleware
|
| 11 |
+
|
| 12 |
+
def _discover_workspace_root() -> Path:
|
| 13 |
+
env_root = os.getenv("CODE_TOOLS_ROOT")
|
| 14 |
+
if env_root:
|
| 15 |
+
return Path(env_root).expanduser().resolve()
|
| 16 |
+
|
| 17 |
+
script_root = Path(__file__).resolve().parents[1]
|
| 18 |
+
if (script_root / ".prefab").exists():
|
| 19 |
+
return script_root
|
| 20 |
+
|
| 21 |
+
return (Path.home() / "source/code_tools").resolve()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
WORKSPACE_ROOT = _discover_workspace_root()
|
| 25 |
+
PREFAB_ROOT = WORKSPACE_ROOT / ".prefab"
|
| 26 |
+
PREFAB_SRC = Path(os.getenv("PREFAB_SRC", str(Path.home() / "source/prefab/src")))
|
| 27 |
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
| 28 |
+
CARDS_DIR = PREFAB_ROOT / "agent-cards"
|
| 29 |
+
CONFIG_PATH = PREFAB_ROOT / "fastagent.config.yaml"
|
| 30 |
+
RAW_CARD_FILE = CARDS_DIR / "hub_search_raw.md"
|
| 31 |
+
PREFAB_NATIVE_CARD_FILE = CARDS_DIR / "hub_search_prefab_native.md"
|
| 32 |
+
PREFAB_LLM_RAW_CARD_FILE = CARDS_DIR / "hub_search_prefab_llm_raw.md"
|
| 33 |
+
PREFAB_LLM_CODEGEN_CARD_FILE = CARDS_DIR / "hub_search_prefab_llm_codegen.md"
|
| 34 |
+
PREFAB_LLM_CHAIN_CARD_FILE = CARDS_DIR / "hub_search_prefab_llm_chain.md"
|
| 35 |
+
RAW_AGENT = "hub_search_raw"
|
| 36 |
+
PREFAB_NATIVE_AGENT = "hub_search_prefab_native"
|
| 37 |
+
PREFAB_LLM_CHAIN_AGENT = "hub_search_prefab_llm_chain"
|
| 38 |
+
|
| 39 |
+
HOST = os.getenv("HOST", "0.0.0.0")
|
| 40 |
+
PORT = int(os.getenv("PORT", "9999"))
|
| 41 |
+
PATH = os.getenv("MCP_PATH", "/mcp")
|
| 42 |
+
CORS_MIDDLEWARE = [
|
| 43 |
+
Middleware(
|
| 44 |
+
CORSMiddleware,
|
| 45 |
+
allow_origins=["*"],
|
| 46 |
+
allow_methods=["*"],
|
| 47 |
+
allow_headers=["*"],
|
| 48 |
+
expose_headers=["mcp-session-id"],
|
| 49 |
+
)
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
os.chdir(WORKSPACE_ROOT)
|
| 53 |
+
if PREFAB_SRC.exists() and str(PREFAB_SRC) not in sys.path:
|
| 54 |
+
sys.path.insert(0, str(PREFAB_SRC))
|
| 55 |
+
if str(SCRIPTS_DIR) not in sys.path:
|
| 56 |
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
| 57 |
+
|
| 58 |
+
from fast_agent import FastAgent
|
| 59 |
+
from fastmcp import FastMCP
|
| 60 |
+
from fastmcp.tools import ToolResult
|
| 61 |
+
from mcp.types import TextContent
|
| 62 |
+
from prefab_hub_ui import (
|
| 63 |
+
build_runtime_wire,
|
| 64 |
+
error_wire,
|
| 65 |
+
parse_passthrough_wire,
|
| 66 |
+
parse_runtime_payload,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
fast = FastAgent(
|
| 70 |
+
"hub-search-prefab",
|
| 71 |
+
config_path=str(CONFIG_PATH),
|
| 72 |
+
parse_cli_args=False,
|
| 73 |
+
)
|
| 74 |
+
fast.load_agents(RAW_CARD_FILE)
|
| 75 |
+
fast.load_agents(PREFAB_NATIVE_CARD_FILE)
|
| 76 |
+
fast.load_agents(PREFAB_LLM_RAW_CARD_FILE)
|
| 77 |
+
fast.load_agents(PREFAB_LLM_CODEGEN_CARD_FILE)
|
| 78 |
+
fast.load_agents(PREFAB_LLM_CHAIN_CARD_FILE)
|
| 79 |
+
|
| 80 |
+
mcp = FastMCP("hub-search-prefab")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
async def _run_raw(query: str) -> str:
|
| 84 |
+
async with fast.run() as agents:
|
| 85 |
+
return await getattr(agents, RAW_AGENT).send(query)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
async def _run_prefab_native(query: str) -> str:
|
| 89 |
+
async with fast.run() as agents:
|
| 90 |
+
return await getattr(agents, PREFAB_NATIVE_AGENT).send(query)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
async def _run_prefab_llm_chain(query: str) -> str:
|
| 94 |
+
async with fast.run() as agents:
|
| 95 |
+
return await getattr(agents, PREFAB_LLM_CHAIN_AGENT).send(query)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _wire_tool_result(wire: dict[str, object]) -> ToolResult:
|
| 99 |
+
return ToolResult(
|
| 100 |
+
content=[TextContent(type="text", text="[Rendered Prefab UI]")],
|
| 101 |
+
structured_content=wire,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _render_query_wire(query: str, raw_text: str) -> dict[str, object]:
|
| 106 |
+
payload = parse_runtime_payload(raw_text)
|
| 107 |
+
return build_runtime_wire(query, payload)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _render_prefab_wire(prefab_text: str) -> dict[str, object]:
|
| 111 |
+
return parse_passthrough_wire(prefab_text)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
async def _build_query_wire(query: str) -> dict[str, object]:
|
| 115 |
+
prefab_response = await _run_prefab_native(query)
|
| 116 |
+
try:
|
| 117 |
+
return _render_prefab_wire(prefab_response)
|
| 118 |
+
except Exception:
|
| 119 |
+
traceback.print_exc()
|
| 120 |
+
raw = await _run_raw(query)
|
| 121 |
+
return _render_query_wire(query, raw)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _missing_query_json() -> str:
|
| 125 |
+
return json.dumps(
|
| 126 |
+
{
|
| 127 |
+
"result": None,
|
| 128 |
+
"meta": {
|
| 129 |
+
"ok": False,
|
| 130 |
+
"error": "Missing required argument: query",
|
| 131 |
+
},
|
| 132 |
+
}
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@mcp.tool(app=True)
|
| 137 |
+
async def hub_search_prefab(query: str) -> ToolResult:
|
| 138 |
+
"""Run the Prefab UI service: model-authored Prefab first, raw deterministic fallback second."""
|
| 139 |
+
try:
|
| 140 |
+
wire = await _build_query_wire(query)
|
| 141 |
+
except Exception as exc: # noqa: BLE001
|
| 142 |
+
traceback.print_exc()
|
| 143 |
+
wire = error_wire(str(exc))
|
| 144 |
+
return _wire_tool_result(wire)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@mcp.tool
|
| 148 |
+
async def hub_search_prefab_native_debug(query: str | None = None) -> str:
|
| 149 |
+
"""Return the one-pass native Prefab agent payload, before fallback rendering."""
|
| 150 |
+
if not query:
|
| 151 |
+
return _missing_query_json()
|
| 152 |
+
try:
|
| 153 |
+
return await _run_prefab_native(query)
|
| 154 |
+
except Exception as exc: # noqa: BLE001
|
| 155 |
+
traceback.print_exc()
|
| 156 |
+
return json.dumps({"result": None, "meta": {"ok": False, "error": str(exc)}})
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@mcp.tool
|
| 160 |
+
async def hub_search_prefab_wire(query: str | None = None) -> str:
|
| 161 |
+
"""Return final Prefab wire JSON after active-path parse and fallback logic."""
|
| 162 |
+
if not query:
|
| 163 |
+
return json.dumps(error_wire("Missing required argument: query"), ensure_ascii=False)
|
| 164 |
+
try:
|
| 165 |
+
wire = await _build_query_wire(query)
|
| 166 |
+
return json.dumps(wire, ensure_ascii=False)
|
| 167 |
+
except Exception as exc: # noqa: BLE001
|
| 168 |
+
traceback.print_exc()
|
| 169 |
+
return json.dumps(error_wire(str(exc)), ensure_ascii=False)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@mcp.tool
|
| 173 |
+
async def hub_search_raw_debug(query: str | None = None) -> str:
|
| 174 |
+
"""Return the raw live-service payload from the raw Hub search agent."""
|
| 175 |
+
if not query:
|
| 176 |
+
return _missing_query_json()
|
| 177 |
+
try:
|
| 178 |
+
return await _run_raw(query)
|
| 179 |
+
except Exception as exc: # noqa: BLE001
|
| 180 |
+
traceback.print_exc()
|
| 181 |
+
return json.dumps({"result": None, "meta": {"ok": False, "error": str(exc)}})
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@mcp.tool
|
| 185 |
+
async def hub_search_prefab_llm_debug(query: str | None = None) -> str:
|
| 186 |
+
"""Return the two-pass LLM chain payload for comparison/debugging."""
|
| 187 |
+
if not query:
|
| 188 |
+
return _missing_query_json()
|
| 189 |
+
try:
|
| 190 |
+
return await _run_prefab_llm_chain(query)
|
| 191 |
+
except Exception as exc: # noqa: BLE001
|
| 192 |
+
traceback.print_exc()
|
| 193 |
+
return json.dumps({"result": None, "meta": {"ok": False, "error": str(exc)}})
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def main() -> None:
|
| 197 |
+
mcp.run(
|
| 198 |
+
"streamable-http",
|
| 199 |
+
host=HOST,
|
| 200 |
+
port=PORT,
|
| 201 |
+
path=PATH,
|
| 202 |
+
stateless_http=True,
|
| 203 |
+
middleware=CORS_MIDDLEWARE,
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
if __name__ == "__main__":
|
| 208 |
+
main()
|
scripts/prefab_hub_ui.py
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ast
|
| 4 |
+
import json
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from prefab_ui.themes import blue
|
| 9 |
+
|
| 10 |
+
PAGE_CSS_CLASS = "w-full max-w-6xl mx-auto p-4 md:p-6 lg:px-8"
|
| 11 |
+
DEFAULT_THEME: dict[str, Any] = blue.to_json()
|
| 12 |
+
|
| 13 |
+
_COMPONENT_KEY_ALIASES: dict[str, str] = {
|
| 14 |
+
"bar_radius": "barRadius",
|
| 15 |
+
"button_type": "buttonType",
|
| 16 |
+
"close_delay": "closeDelay",
|
| 17 |
+
"col_span": "colSpan",
|
| 18 |
+
"column_template": "columnTemplate",
|
| 19 |
+
"css_class": "cssClass",
|
| 20 |
+
"data_key": "dataKey",
|
| 21 |
+
"default_value": "defaultValue",
|
| 22 |
+
"end_angle": "endAngle",
|
| 23 |
+
"inner_radius": "innerRadius",
|
| 24 |
+
"name_key": "nameKey",
|
| 25 |
+
"min_column_width": "minColumnWidth",
|
| 26 |
+
"on_change": "onChange",
|
| 27 |
+
"on_click": "onClick",
|
| 28 |
+
"on_submit": "onSubmit",
|
| 29 |
+
"open_delay": "openDelay",
|
| 30 |
+
"padding_angle": "paddingAngle",
|
| 31 |
+
"row_height": "rowHeight",
|
| 32 |
+
"row_span": "rowSpan",
|
| 33 |
+
"show_dots": "showDots",
|
| 34 |
+
"show_grid": "showGrid",
|
| 35 |
+
"show_label": "showLabel",
|
| 36 |
+
"show_legend": "showLegend",
|
| 37 |
+
"show_tooltip": "showTooltip",
|
| 38 |
+
"show_y_axis": "showYAxis",
|
| 39 |
+
"start_angle": "startAngle",
|
| 40 |
+
"trend_sentiment": "trendSentiment",
|
| 41 |
+
"x_axis": "xAxis",
|
| 42 |
+
"y_axis_format": "yAxisFormat",
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
_DATATABLE_KEY_ALIASES: dict[str, str] = {
|
| 46 |
+
"on_row_click": "onRowClick",
|
| 47 |
+
"page_size": "pageSize",
|
| 48 |
+
"searchable": "search",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
_DATATABLE_COLUMN_KEY_ALIASES: dict[str, str] = {
|
| 52 |
+
"cell_class": "cellClass",
|
| 53 |
+
"header_class": "headerClass",
|
| 54 |
+
"max_width": "maxWidth",
|
| 55 |
+
"min_width": "minWidth",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
_PREFERRED_COLUMN_ORDER: tuple[str, ...] = (
|
| 59 |
+
"repo_id",
|
| 60 |
+
"title",
|
| 61 |
+
"name",
|
| 62 |
+
"username",
|
| 63 |
+
"handle",
|
| 64 |
+
"owner",
|
| 65 |
+
"author",
|
| 66 |
+
"repo_type",
|
| 67 |
+
"type",
|
| 68 |
+
"role",
|
| 69 |
+
"status",
|
| 70 |
+
"likes",
|
| 71 |
+
"downloads",
|
| 72 |
+
"followers_count",
|
| 73 |
+
"following_count",
|
| 74 |
+
"item_count",
|
| 75 |
+
"count",
|
| 76 |
+
"created_at",
|
| 77 |
+
"published_at",
|
| 78 |
+
"timestamp",
|
| 79 |
+
"last_modified",
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
_PREFERRED_METRIC_KEYS: tuple[str, ...] = (
|
| 83 |
+
"count",
|
| 84 |
+
"total",
|
| 85 |
+
"returned",
|
| 86 |
+
"matched",
|
| 87 |
+
"scanned",
|
| 88 |
+
"likes",
|
| 89 |
+
"downloads",
|
| 90 |
+
"followers_count",
|
| 91 |
+
"following_count",
|
| 92 |
+
"members_count",
|
| 93 |
+
"models_count",
|
| 94 |
+
"datasets_count",
|
| 95 |
+
"spaces_count",
|
| 96 |
+
"item_count",
|
| 97 |
+
"api_calls",
|
| 98 |
+
"elapsed_ms",
|
| 99 |
+
"pro_likers",
|
| 100 |
+
"normal_likers",
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
_URL_KEYS: tuple[str, ...] = (
|
| 104 |
+
"repo_url",
|
| 105 |
+
"url",
|
| 106 |
+
"html_url",
|
| 107 |
+
"website_url",
|
| 108 |
+
"project_page_url",
|
| 109 |
+
"github_repo_url",
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _copy_default_theme() -> dict[str, Any]:
|
| 114 |
+
return deepcopy(DEFAULT_THEME)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _rename_keys(value: dict[str, Any], aliases: dict[str, str]) -> dict[str, Any]:
|
| 118 |
+
renamed = dict(value)
|
| 119 |
+
for old_key, new_key in aliases.items():
|
| 120 |
+
if old_key in renamed and new_key not in renamed:
|
| 121 |
+
renamed[new_key] = renamed.pop(old_key)
|
| 122 |
+
return renamed
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def normalize_prefab_wire(value: Any) -> Any:
|
| 126 |
+
if isinstance(value, list):
|
| 127 |
+
return [normalize_prefab_wire(item) for item in value]
|
| 128 |
+
|
| 129 |
+
if not isinstance(value, dict):
|
| 130 |
+
return value
|
| 131 |
+
|
| 132 |
+
normalized = {key: normalize_prefab_wire(item) for key, item in value.items()}
|
| 133 |
+
component_type = normalized.get("type")
|
| 134 |
+
|
| 135 |
+
if isinstance(component_type, str):
|
| 136 |
+
normalized = _rename_keys(normalized, _COMPONENT_KEY_ALIASES)
|
| 137 |
+
|
| 138 |
+
if component_type == "Metric" and "title" in normalized and "label" not in normalized:
|
| 139 |
+
normalized["label"] = normalized.pop("title")
|
| 140 |
+
|
| 141 |
+
if component_type == "DataTable":
|
| 142 |
+
normalized = _rename_keys(normalized, _DATATABLE_KEY_ALIASES)
|
| 143 |
+
|
| 144 |
+
if "data" in normalized and "rows" not in normalized:
|
| 145 |
+
normalized["rows"] = normalized.pop("data")
|
| 146 |
+
|
| 147 |
+
if isinstance(normalized.get("columns"), list):
|
| 148 |
+
fixed_columns: list[Any] = []
|
| 149 |
+
for column in normalized["columns"]:
|
| 150 |
+
if isinstance(column, dict):
|
| 151 |
+
fixed_column = _rename_keys(dict(column), _DATATABLE_COLUMN_KEY_ALIASES)
|
| 152 |
+
if "accessor" in fixed_column and "key" not in fixed_column:
|
| 153 |
+
fixed_column["key"] = fixed_column.pop("accessor")
|
| 154 |
+
if "title" in fixed_column and "header" not in fixed_column:
|
| 155 |
+
fixed_column["header"] = fixed_column.pop("title")
|
| 156 |
+
if "sortable" not in fixed_column:
|
| 157 |
+
fixed_column["sortable"] = fixed_column.get("key") not in {"description"}
|
| 158 |
+
fixed_columns.append(fixed_column)
|
| 159 |
+
else:
|
| 160 |
+
fixed_columns.append(column)
|
| 161 |
+
normalized["columns"] = fixed_columns
|
| 162 |
+
|
| 163 |
+
if "view" in normalized and isinstance(normalized["view"], list):
|
| 164 |
+
normalized["view"] = {
|
| 165 |
+
"type": "Column",
|
| 166 |
+
"gap": 6,
|
| 167 |
+
"cssClass": PAGE_CSS_CLASS,
|
| 168 |
+
"children": normalized["view"],
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
return normalized
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def error_wire(message: str) -> dict[str, Any]:
|
| 175 |
+
return {
|
| 176 |
+
"version": "0.2",
|
| 177 |
+
"theme": _copy_default_theme(),
|
| 178 |
+
"view": {
|
| 179 |
+
"type": "Alert",
|
| 180 |
+
"variant": "destructive",
|
| 181 |
+
"icon": "circle-x",
|
| 182 |
+
"children": [
|
| 183 |
+
{"type": "AlertTitle", "content": "Prefab demo error"},
|
| 184 |
+
{"type": "AlertDescription", "content": message},
|
| 185 |
+
],
|
| 186 |
+
},
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def extract_prefab_body_parts(
|
| 191 |
+
raw_value: Any,
|
| 192 |
+
) -> tuple[dict[str, Any], dict[str, Any] | None, dict[str, Any] | None, dict[str, Any] | None]:
|
| 193 |
+
normalized = normalize_prefab_wire(raw_value)
|
| 194 |
+
|
| 195 |
+
if isinstance(normalized, dict) and isinstance(normalized.get("view"), dict):
|
| 196 |
+
return (
|
| 197 |
+
normalized["view"],
|
| 198 |
+
normalized.get("state") if isinstance(normalized.get("state"), dict) else None,
|
| 199 |
+
normalized.get("defs") if isinstance(normalized.get("defs"), dict) else None,
|
| 200 |
+
normalized.get("theme") if isinstance(normalized.get("theme"), dict) else None,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
if isinstance(normalized, dict) and normalized.get("type"):
|
| 204 |
+
return normalized, None, None, None
|
| 205 |
+
|
| 206 |
+
if isinstance(normalized, list):
|
| 207 |
+
return {"type": "Column", "gap": 6, "children": normalized}, None, None, None
|
| 208 |
+
|
| 209 |
+
raise ValueError("payload did not contain a Prefab body or wire object")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def build_runtime_sections(meta: dict[str, Any] | None) -> list[dict[str, Any]]:
|
| 213 |
+
if not isinstance(meta, dict) or not meta:
|
| 214 |
+
return []
|
| 215 |
+
|
| 216 |
+
sections: list[dict[str, Any]] = []
|
| 217 |
+
if meta.get("limits_reached"):
|
| 218 |
+
sections.append(
|
| 219 |
+
{
|
| 220 |
+
"type": "Alert",
|
| 221 |
+
"variant": "warning",
|
| 222 |
+
"icon": "circle-alert",
|
| 223 |
+
"children": [
|
| 224 |
+
{"type": "AlertTitle", "content": "Runtime limits reached"},
|
| 225 |
+
{
|
| 226 |
+
"type": "AlertDescription",
|
| 227 |
+
"content": "The result was produced under runtime limits. See execution metadata for details.",
|
| 228 |
+
},
|
| 229 |
+
],
|
| 230 |
+
}
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
summary_children: list[dict[str, Any]] = [{"type": "Small", "content": "Execution metadata"}]
|
| 234 |
+
if meta.get("api_calls") is not None:
|
| 235 |
+
summary_children.append(
|
| 236 |
+
{
|
| 237 |
+
"type": "Badge",
|
| 238 |
+
"label": f'{meta["api_calls"]} calls',
|
| 239 |
+
"variant": "outline",
|
| 240 |
+
}
|
| 241 |
+
)
|
| 242 |
+
if meta.get("elapsed_ms") is not None:
|
| 243 |
+
summary_children.append(
|
| 244 |
+
{
|
| 245 |
+
"type": "Badge",
|
| 246 |
+
"label": f'{meta["elapsed_ms"]} ms',
|
| 247 |
+
"variant": "outline",
|
| 248 |
+
}
|
| 249 |
+
)
|
| 250 |
+
if meta.get("limits_reached"):
|
| 251 |
+
summary_children.append(
|
| 252 |
+
{
|
| 253 |
+
"type": "Badge",
|
| 254 |
+
"label": "limits reached",
|
| 255 |
+
"variant": "secondary",
|
| 256 |
+
}
|
| 257 |
+
)
|
| 258 |
+
else:
|
| 259 |
+
summary_children.append(
|
| 260 |
+
{
|
| 261 |
+
"type": "Muted",
|
| 262 |
+
"content": "No runtime limits reached",
|
| 263 |
+
}
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
if len(summary_children) > 1:
|
| 267 |
+
sections.append(
|
| 268 |
+
{
|
| 269 |
+
"type": "Column",
|
| 270 |
+
"gap": 2,
|
| 271 |
+
"children": [
|
| 272 |
+
{"type": "Separator", "orientation": "horizontal"},
|
| 273 |
+
{
|
| 274 |
+
"type": "Row",
|
| 275 |
+
"gap": 2,
|
| 276 |
+
"align": "center",
|
| 277 |
+
"cssClass": "flex-wrap",
|
| 278 |
+
"children": summary_children,
|
| 279 |
+
},
|
| 280 |
+
],
|
| 281 |
+
}
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
limit_summary = meta.get("limit_summary")
|
| 285 |
+
if isinstance(limit_summary, list) and limit_summary:
|
| 286 |
+
parts: list[str] = []
|
| 287 |
+
for item in limit_summary:
|
| 288 |
+
if not isinstance(item, dict):
|
| 289 |
+
continue
|
| 290 |
+
helper = str(item.get("helper") or item.get("source") or "unknown")
|
| 291 |
+
returned = item.get("returned")
|
| 292 |
+
total = item.get("total")
|
| 293 |
+
truncated_by = str(item.get("truncated_by") or "").strip()
|
| 294 |
+
hint = str(item.get("next_request_hint") or "").strip()
|
| 295 |
+
|
| 296 |
+
detail_bits: list[str] = []
|
| 297 |
+
if returned is not None and total is not None:
|
| 298 |
+
detail_bits.append(f"{returned}/{total}")
|
| 299 |
+
elif returned is not None:
|
| 300 |
+
detail_bits.append(str(returned))
|
| 301 |
+
if truncated_by:
|
| 302 |
+
detail_bits.append(truncated_by.replace("_", " "))
|
| 303 |
+
detail = ", ".join(detail_bits)
|
| 304 |
+
part = helper if not detail else f"{helper} ({detail})"
|
| 305 |
+
if hint:
|
| 306 |
+
part = f"{part}: {hint}"
|
| 307 |
+
parts.append(part)
|
| 308 |
+
|
| 309 |
+
if parts:
|
| 310 |
+
sections.append(
|
| 311 |
+
{
|
| 312 |
+
"type": "Alert",
|
| 313 |
+
"variant": "warning",
|
| 314 |
+
"icon": "circle-alert",
|
| 315 |
+
"children": [
|
| 316 |
+
{"type": "AlertTitle", "content": "Compact limit summary"},
|
| 317 |
+
{"type": "AlertDescription", "content": " • ".join(parts[:3])},
|
| 318 |
+
],
|
| 319 |
+
}
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
return sections
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _looks_like_page_root(component: dict[str, Any]) -> bool:
|
| 326 |
+
if component.get("type") != "Column":
|
| 327 |
+
return False
|
| 328 |
+
if not isinstance(component.get("children"), list):
|
| 329 |
+
return False
|
| 330 |
+
css_class = str(component.get("cssClass") or "")
|
| 331 |
+
return "max-w-" in css_class and "mx-auto" in css_class
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def wrap_prefab_body(
|
| 335 |
+
body_view: dict[str, Any],
|
| 336 |
+
*,
|
| 337 |
+
meta: dict[str, Any] | None = None,
|
| 338 |
+
state: dict[str, Any] | None = None,
|
| 339 |
+
defs: dict[str, Any] | None = None,
|
| 340 |
+
theme: dict[str, Any] | None = None,
|
| 341 |
+
) -> dict[str, Any]:
|
| 342 |
+
runtime_sections = build_runtime_sections(meta)
|
| 343 |
+
if _looks_like_page_root(body_view):
|
| 344 |
+
children = list(body_view.get("children") or [])
|
| 345 |
+
view = dict(body_view)
|
| 346 |
+
view.setdefault("gap", 6)
|
| 347 |
+
view.setdefault("cssClass", PAGE_CSS_CLASS)
|
| 348 |
+
view["children"] = [*children, *runtime_sections]
|
| 349 |
+
else:
|
| 350 |
+
view = {
|
| 351 |
+
"type": "Column",
|
| 352 |
+
"gap": 6,
|
| 353 |
+
"cssClass": PAGE_CSS_CLASS,
|
| 354 |
+
"children": [body_view, *runtime_sections],
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
wire: dict[str, Any] = {
|
| 358 |
+
"version": "0.2",
|
| 359 |
+
"theme": theme if theme is not None else _copy_default_theme(),
|
| 360 |
+
"view": view,
|
| 361 |
+
}
|
| 362 |
+
if state:
|
| 363 |
+
wire["state"] = state
|
| 364 |
+
if defs:
|
| 365 |
+
wire["defs"] = defs
|
| 366 |
+
return wire
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def coerce_wire(raw_value: Any) -> dict[str, Any]:
|
| 370 |
+
normalized = normalize_prefab_wire(raw_value)
|
| 371 |
+
|
| 372 |
+
if isinstance(normalized, dict) and isinstance(normalized.get("view"), dict):
|
| 373 |
+
wire = dict(normalized)
|
| 374 |
+
wire.setdefault("version", "0.2")
|
| 375 |
+
if not isinstance(wire.get("theme"), dict):
|
| 376 |
+
wire["theme"] = _copy_default_theme()
|
| 377 |
+
return wire
|
| 378 |
+
|
| 379 |
+
body_view, state, defs, theme = extract_prefab_body_parts(normalized)
|
| 380 |
+
return wrap_prefab_body(
|
| 381 |
+
body_view,
|
| 382 |
+
state=state,
|
| 383 |
+
defs=defs,
|
| 384 |
+
theme=theme,
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def _strip_code_fence(text: str) -> str:
|
| 389 |
+
stripped = text.strip()
|
| 390 |
+
if not stripped.startswith("```"):
|
| 391 |
+
return stripped
|
| 392 |
+
|
| 393 |
+
lines = stripped.splitlines()
|
| 394 |
+
if not lines:
|
| 395 |
+
return stripped
|
| 396 |
+
if lines[0].startswith("```"):
|
| 397 |
+
lines = lines[1:]
|
| 398 |
+
if lines and lines[-1].strip() == "```":
|
| 399 |
+
lines = lines[:-1]
|
| 400 |
+
return "\n".join(lines).strip()
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def parse_jsonish_value(text: str) -> Any:
|
| 404 |
+
candidate = _strip_code_fence(text)
|
| 405 |
+
if not candidate:
|
| 406 |
+
raise ValueError("empty payload")
|
| 407 |
+
|
| 408 |
+
try:
|
| 409 |
+
return json.loads(candidate)
|
| 410 |
+
except json.JSONDecodeError:
|
| 411 |
+
try:
|
| 412 |
+
return ast.literal_eval(candidate)
|
| 413 |
+
except (SyntaxError, ValueError) as exc:
|
| 414 |
+
raise ValueError("payload was not valid JSON or Python literal data") from exc
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def parse_runtime_payload(text: str) -> dict[str, Any]:
|
| 418 |
+
payload = parse_jsonish_value(text)
|
| 419 |
+
if not isinstance(payload, dict):
|
| 420 |
+
raise ValueError("runtime payload was not an object")
|
| 421 |
+
return payload
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def _titleize(key: str) -> str:
|
| 425 |
+
text = key.replace("_", " ").replace("-", " ").strip()
|
| 426 |
+
return " ".join(part.capitalize() for part in text.split()) or key
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def _truncate(text: str, limit: int = 160) -> str:
|
| 430 |
+
if len(text) <= limit:
|
| 431 |
+
return text
|
| 432 |
+
return f"{text[: limit - 1]}…"
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def _compact_text(value: Any, limit: int = 160) -> str:
|
| 436 |
+
if value is None:
|
| 437 |
+
return "None"
|
| 438 |
+
if isinstance(value, str):
|
| 439 |
+
collapsed = " ".join(value.split())
|
| 440 |
+
return _truncate(collapsed, limit=limit)
|
| 441 |
+
if isinstance(value, (int, float, bool)):
|
| 442 |
+
return str(value)
|
| 443 |
+
if isinstance(value, list):
|
| 444 |
+
if value and all(isinstance(item, (str, int, float, bool)) or item is None for item in value):
|
| 445 |
+
collapsed = ", ".join(_compact_text(item, limit=40) for item in value[:6])
|
| 446 |
+
if len(value) > 6:
|
| 447 |
+
collapsed = f"{collapsed}, …"
|
| 448 |
+
return _truncate(collapsed, limit=limit)
|
| 449 |
+
return _truncate(json.dumps(value, ensure_ascii=False, sort_keys=True), limit=limit)
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _is_scalar(value: Any) -> bool:
|
| 453 |
+
if value is None or isinstance(value, (str, int, float, bool)):
|
| 454 |
+
return True
|
| 455 |
+
if isinstance(value, list):
|
| 456 |
+
return all(item is None or isinstance(item, (str, int, float, bool)) for item in value)
|
| 457 |
+
return False
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def _normalize_cell(value: Any) -> Any:
|
| 461 |
+
if value is None or isinstance(value, (str, int, float, bool)):
|
| 462 |
+
return value
|
| 463 |
+
return _compact_text(value)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def _normalize_row(row: dict[str, Any]) -> dict[str, Any]:
|
| 467 |
+
return {str(key): _normalize_cell(value) for key, value in row.items()}
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def _column_rank(key: str) -> tuple[int, str]:
|
| 471 |
+
try:
|
| 472 |
+
return (_PREFERRED_COLUMN_ORDER.index(key), key)
|
| 473 |
+
except ValueError:
|
| 474 |
+
return (len(_PREFERRED_COLUMN_ORDER), key)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def _metric_rank(key: str) -> tuple[int, str]:
|
| 478 |
+
try:
|
| 479 |
+
return (_PREFERRED_METRIC_KEYS.index(key), key)
|
| 480 |
+
except ValueError:
|
| 481 |
+
return (len(_PREFERRED_METRIC_KEYS), key)
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
def _build_row_click(rows: list[dict[str, Any]]) -> dict[str, Any] | None:
|
| 485 |
+
for key in _URL_KEYS:
|
| 486 |
+
if any(isinstance(row.get(key), str) and row.get(key) for row in rows):
|
| 487 |
+
return {
|
| 488 |
+
"action": "openLink",
|
| 489 |
+
"url": f"{{{{ $event.{key} }}}}",
|
| 490 |
+
}
|
| 491 |
+
return None
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def _build_table_card(
|
| 495 |
+
title: str,
|
| 496 |
+
rows: list[dict[str, Any]],
|
| 497 |
+
*,
|
| 498 |
+
description: str | None = None,
|
| 499 |
+
empty_message: str = "No rows returned.",
|
| 500 |
+
) -> dict[str, Any]:
|
| 501 |
+
if not rows:
|
| 502 |
+
return {
|
| 503 |
+
"type": "Card",
|
| 504 |
+
"children": [
|
| 505 |
+
{
|
| 506 |
+
"type": "CardHeader",
|
| 507 |
+
"children": [
|
| 508 |
+
{"type": "CardTitle", "content": title},
|
| 509 |
+
*(
|
| 510 |
+
[{"type": "CardDescription", "content": description}]
|
| 511 |
+
if description
|
| 512 |
+
else []
|
| 513 |
+
),
|
| 514 |
+
],
|
| 515 |
+
},
|
| 516 |
+
{
|
| 517 |
+
"type": "CardContent",
|
| 518 |
+
"children": [
|
| 519 |
+
{
|
| 520 |
+
"type": "Alert",
|
| 521 |
+
"variant": "default",
|
| 522 |
+
"children": [
|
| 523 |
+
{"type": "AlertTitle", "content": title},
|
| 524 |
+
{"type": "AlertDescription", "content": empty_message},
|
| 525 |
+
],
|
| 526 |
+
}
|
| 527 |
+
],
|
| 528 |
+
},
|
| 529 |
+
],
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
normalized_rows = [_normalize_row(row) for row in rows]
|
| 533 |
+
all_keys = {key for row in normalized_rows for key in row}
|
| 534 |
+
visible_keys = sorted(all_keys, key=_column_rank)[:8]
|
| 535 |
+
columns: list[dict[str, Any]] = []
|
| 536 |
+
for key in visible_keys:
|
| 537 |
+
column: dict[str, Any] = {
|
| 538 |
+
"key": key,
|
| 539 |
+
"header": _titleize(key),
|
| 540 |
+
"sortable": key not in {"description"},
|
| 541 |
+
}
|
| 542 |
+
if any(isinstance(row.get(key), (int, float)) for row in normalized_rows):
|
| 543 |
+
column["align"] = "right"
|
| 544 |
+
column["format"] = "number"
|
| 545 |
+
if key in {"description"}:
|
| 546 |
+
column["maxWidth"] = "28rem"
|
| 547 |
+
column["sortable"] = False
|
| 548 |
+
columns.append(column)
|
| 549 |
+
|
| 550 |
+
data_table: dict[str, Any] = {
|
| 551 |
+
"type": "DataTable",
|
| 552 |
+
"columns": columns,
|
| 553 |
+
"rows": normalized_rows,
|
| 554 |
+
"search": len(normalized_rows) > 8,
|
| 555 |
+
"paginated": len(normalized_rows) > 10,
|
| 556 |
+
"pageSize": 10,
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
row_click = _build_row_click(rows)
|
| 560 |
+
if row_click is not None:
|
| 561 |
+
data_table["onRowClick"] = row_click
|
| 562 |
+
|
| 563 |
+
return {
|
| 564 |
+
"type": "Card",
|
| 565 |
+
"children": [
|
| 566 |
+
{
|
| 567 |
+
"type": "CardHeader",
|
| 568 |
+
"children": [
|
| 569 |
+
{"type": "CardTitle", "content": title},
|
| 570 |
+
*(
|
| 571 |
+
[{"type": "CardDescription", "content": description}]
|
| 572 |
+
if description
|
| 573 |
+
else []
|
| 574 |
+
),
|
| 575 |
+
],
|
| 576 |
+
},
|
| 577 |
+
{
|
| 578 |
+
"type": "CardContent",
|
| 579 |
+
"children": [data_table],
|
| 580 |
+
},
|
| 581 |
+
],
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def _build_key_value_card(
|
| 586 |
+
title: str,
|
| 587 |
+
values: dict[str, Any],
|
| 588 |
+
*,
|
| 589 |
+
description: str | None = None,
|
| 590 |
+
) -> dict[str, Any]:
|
| 591 |
+
rows = [{"field": _titleize(key), "value": _normalize_cell(value)} for key, value in values.items()]
|
| 592 |
+
return _build_table_card(
|
| 593 |
+
title,
|
| 594 |
+
rows,
|
| 595 |
+
description=description,
|
| 596 |
+
empty_message="No fields available.",
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
def _select_metric_keys(values: dict[str, Any]) -> list[str]:
|
| 601 |
+
numeric_or_bool = [
|
| 602 |
+
key
|
| 603 |
+
for key, value in values.items()
|
| 604 |
+
if isinstance(value, (int, float, bool)) and not isinstance(value, str)
|
| 605 |
+
]
|
| 606 |
+
prioritized = sorted(numeric_or_bool, key=_metric_rank)
|
| 607 |
+
return prioritized[:4]
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
def _build_metric_grid(title: str, values: dict[str, Any]) -> dict[str, Any] | None:
|
| 611 |
+
metric_keys = _select_metric_keys(values)
|
| 612 |
+
if not metric_keys:
|
| 613 |
+
return None
|
| 614 |
+
|
| 615 |
+
tiles = [
|
| 616 |
+
{
|
| 617 |
+
"type": "Card",
|
| 618 |
+
"children": [
|
| 619 |
+
{
|
| 620 |
+
"type": "CardContent",
|
| 621 |
+
"cssClass": "p-6",
|
| 622 |
+
"children": [
|
| 623 |
+
{
|
| 624 |
+
"type": "Metric",
|
| 625 |
+
"label": _titleize(key),
|
| 626 |
+
"value": values[key],
|
| 627 |
+
}
|
| 628 |
+
],
|
| 629 |
+
}
|
| 630 |
+
],
|
| 631 |
+
}
|
| 632 |
+
for key in metric_keys
|
| 633 |
+
]
|
| 634 |
+
|
| 635 |
+
return {
|
| 636 |
+
"type": "Card",
|
| 637 |
+
"children": [
|
| 638 |
+
{
|
| 639 |
+
"type": "CardHeader",
|
| 640 |
+
"children": [{"type": "CardTitle", "content": title}],
|
| 641 |
+
},
|
| 642 |
+
{
|
| 643 |
+
"type": "CardContent",
|
| 644 |
+
"children": [
|
| 645 |
+
{
|
| 646 |
+
"type": "Grid",
|
| 647 |
+
"gap": 4,
|
| 648 |
+
"minColumnWidth": "12rem",
|
| 649 |
+
"children": tiles,
|
| 650 |
+
}
|
| 651 |
+
],
|
| 652 |
+
},
|
| 653 |
+
],
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
|
| 657 |
+
def _looks_like_helper_envelope(value: Any) -> bool:
|
| 658 |
+
return isinstance(value, dict) and {"ok", "items", "item", "meta", "error"}.issubset(value.keys())
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
def build_helper_sections(meta: dict[str, Any] | None) -> list[dict[str, Any]]:
|
| 662 |
+
if not isinstance(meta, dict) or not meta:
|
| 663 |
+
return []
|
| 664 |
+
|
| 665 |
+
sections: list[dict[str, Any]] = []
|
| 666 |
+
warning_bits: list[str] = []
|
| 667 |
+
|
| 668 |
+
if meta.get("error"):
|
| 669 |
+
warning_bits.append(str(meta["error"]))
|
| 670 |
+
if meta.get("truncated"):
|
| 671 |
+
truncated_by = str(meta.get("truncated_by") or "limit").replace("_", " ")
|
| 672 |
+
warning_bits.append(f"Helper output was truncated by {truncated_by}.")
|
| 673 |
+
if meta.get("lower_bound"):
|
| 674 |
+
warning_bits.append("Counts may be lower bounds rather than exact totals.")
|
| 675 |
+
if meta.get("more_available") is True:
|
| 676 |
+
warning_bits.append("More matching rows are available than were returned.")
|
| 677 |
+
if meta.get("more_available") == "unknown":
|
| 678 |
+
warning_bits.append("More matching rows may be available.")
|
| 679 |
+
if meta.get("next_request_hint"):
|
| 680 |
+
warning_bits.append(str(meta["next_request_hint"]))
|
| 681 |
+
|
| 682 |
+
if warning_bits:
|
| 683 |
+
sections.append(
|
| 684 |
+
{
|
| 685 |
+
"type": "Alert",
|
| 686 |
+
"variant": "warning",
|
| 687 |
+
"icon": "circle-alert",
|
| 688 |
+
"children": [
|
| 689 |
+
{"type": "AlertTitle", "content": "Helper coverage notes"},
|
| 690 |
+
{"type": "AlertDescription", "content": " ".join(warning_bits[:3])},
|
| 691 |
+
],
|
| 692 |
+
}
|
| 693 |
+
)
|
| 694 |
+
|
| 695 |
+
badges: list[dict[str, Any]] = [{"type": "Small", "content": "Helper metadata"}]
|
| 696 |
+
for key in ("returned", "matched", "total", "scanned"):
|
| 697 |
+
value = meta.get(key)
|
| 698 |
+
if value is not None:
|
| 699 |
+
badges.append(
|
| 700 |
+
{
|
| 701 |
+
"type": "Badge",
|
| 702 |
+
"label": f"{key}: {value}",
|
| 703 |
+
"variant": "outline",
|
| 704 |
+
}
|
| 705 |
+
)
|
| 706 |
+
source = meta.get("source")
|
| 707 |
+
if isinstance(source, str) and source:
|
| 708 |
+
badges.append({"type": "Muted", "content": source})
|
| 709 |
+
|
| 710 |
+
if len(badges) > 1:
|
| 711 |
+
sections.append(
|
| 712 |
+
{
|
| 713 |
+
"type": "Row",
|
| 714 |
+
"gap": 2,
|
| 715 |
+
"align": "center",
|
| 716 |
+
"cssClass": "flex-wrap",
|
| 717 |
+
"children": badges,
|
| 718 |
+
}
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
+
return sections
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
def _render_list(
|
| 725 |
+
title: str,
|
| 726 |
+
value: list[Any],
|
| 727 |
+
*,
|
| 728 |
+
description: str | None = None,
|
| 729 |
+
depth: int = 0,
|
| 730 |
+
) -> list[dict[str, Any]]:
|
| 731 |
+
if not value:
|
| 732 |
+
return [
|
| 733 |
+
{
|
| 734 |
+
"type": "Alert",
|
| 735 |
+
"variant": "default",
|
| 736 |
+
"children": [
|
| 737 |
+
{"type": "AlertTitle", "content": title},
|
| 738 |
+
{"type": "AlertDescription", "content": "No results returned."},
|
| 739 |
+
],
|
| 740 |
+
}
|
| 741 |
+
]
|
| 742 |
+
|
| 743 |
+
if all(isinstance(item, dict) for item in value):
|
| 744 |
+
rows = [item for item in value if isinstance(item, dict)]
|
| 745 |
+
return [_build_table_card(title, rows, description=description)]
|
| 746 |
+
|
| 747 |
+
rows = [
|
| 748 |
+
{
|
| 749 |
+
"index": index + 1,
|
| 750 |
+
"value": _normalize_cell(item),
|
| 751 |
+
}
|
| 752 |
+
for index, item in enumerate(value)
|
| 753 |
+
]
|
| 754 |
+
return [_build_table_card(title, rows, description=description)]
|
| 755 |
+
|
| 756 |
+
|
| 757 |
+
def _render_dict(
|
| 758 |
+
title: str,
|
| 759 |
+
value: dict[str, Any],
|
| 760 |
+
*,
|
| 761 |
+
description: str | None = None,
|
| 762 |
+
depth: int = 0,
|
| 763 |
+
) -> list[dict[str, Any]]:
|
| 764 |
+
if depth > 2:
|
| 765 |
+
return [_build_key_value_card(title, value, description=description)]
|
| 766 |
+
|
| 767 |
+
if "results" in value or "coverage" in value:
|
| 768 |
+
sections: list[dict[str, Any]] = []
|
| 769 |
+
results = value.get("results")
|
| 770 |
+
coverage = value.get("coverage")
|
| 771 |
+
if results is not None:
|
| 772 |
+
sections.extend(_render_value("Results", results, depth=depth + 1))
|
| 773 |
+
if isinstance(coverage, dict):
|
| 774 |
+
sections.append(_build_key_value_card("Coverage", coverage))
|
| 775 |
+
remaining = {
|
| 776 |
+
key: item for key, item in value.items() if key not in {"results", "coverage"}
|
| 777 |
+
}
|
| 778 |
+
if remaining:
|
| 779 |
+
sections.extend(_render_dict(title, remaining, description=description, depth=depth + 1))
|
| 780 |
+
return sections
|
| 781 |
+
|
| 782 |
+
scalar_items = {key: item for key, item in value.items() if _is_scalar(item)}
|
| 783 |
+
nested_items = {key: item for key, item in value.items() if key not in scalar_items}
|
| 784 |
+
|
| 785 |
+
sections: list[dict[str, Any]] = []
|
| 786 |
+
metric_grid = _build_metric_grid(title, scalar_items)
|
| 787 |
+
metric_keys = set(_select_metric_keys(scalar_items))
|
| 788 |
+
if metric_grid is not None:
|
| 789 |
+
sections.append(metric_grid)
|
| 790 |
+
|
| 791 |
+
remaining_scalars = {
|
| 792 |
+
key: item for key, item in scalar_items.items() if key not in metric_keys
|
| 793 |
+
}
|
| 794 |
+
if remaining_scalars:
|
| 795 |
+
sections.append(_build_key_value_card(title, remaining_scalars, description=description))
|
| 796 |
+
|
| 797 |
+
for key, item in nested_items.items():
|
| 798 |
+
sections.extend(_render_value(_titleize(key), item, depth=depth + 1))
|
| 799 |
+
|
| 800 |
+
if not sections:
|
| 801 |
+
sections.append(_build_key_value_card(title, value, description=description))
|
| 802 |
+
|
| 803 |
+
return sections
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def _render_scalar(
|
| 807 |
+
title: str,
|
| 808 |
+
value: Any,
|
| 809 |
+
*,
|
| 810 |
+
description: str | None = None,
|
| 811 |
+
) -> list[dict[str, Any]]:
|
| 812 |
+
return [
|
| 813 |
+
{
|
| 814 |
+
"type": "Card",
|
| 815 |
+
"children": [
|
| 816 |
+
{
|
| 817 |
+
"type": "CardHeader",
|
| 818 |
+
"children": [
|
| 819 |
+
{"type": "CardTitle", "content": title},
|
| 820 |
+
*(
|
| 821 |
+
[{"type": "CardDescription", "content": description}]
|
| 822 |
+
if description
|
| 823 |
+
else []
|
| 824 |
+
),
|
| 825 |
+
],
|
| 826 |
+
},
|
| 827 |
+
{
|
| 828 |
+
"type": "CardContent",
|
| 829 |
+
"children": [{"type": "Text", "content": _compact_text(value, limit=500)}],
|
| 830 |
+
},
|
| 831 |
+
],
|
| 832 |
+
}
|
| 833 |
+
]
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def _render_value(
|
| 837 |
+
title: str,
|
| 838 |
+
value: Any,
|
| 839 |
+
*,
|
| 840 |
+
description: str | None = None,
|
| 841 |
+
depth: int = 0,
|
| 842 |
+
) -> list[dict[str, Any]]:
|
| 843 |
+
if value is None:
|
| 844 |
+
return _render_scalar(title, "No result returned.", description=description)
|
| 845 |
+
if isinstance(value, dict):
|
| 846 |
+
return _render_dict(title, value, description=description, depth=depth)
|
| 847 |
+
if isinstance(value, list):
|
| 848 |
+
return _render_list(title, value, description=description, depth=depth)
|
| 849 |
+
return _render_scalar(title, value, description=description)
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def _result_count(value: Any) -> int | None:
|
| 853 |
+
if isinstance(value, list):
|
| 854 |
+
return len(value)
|
| 855 |
+
if _looks_like_helper_envelope(value):
|
| 856 |
+
items = value.get("items")
|
| 857 |
+
if isinstance(items, list):
|
| 858 |
+
return len(items)
|
| 859 |
+
if isinstance(value, dict) and isinstance(value.get("results"), list):
|
| 860 |
+
return len(value["results"])
|
| 861 |
+
return None
|
| 862 |
+
|
| 863 |
+
|
| 864 |
+
def _build_summary_card(
|
| 865 |
+
query: str,
|
| 866 |
+
*,
|
| 867 |
+
runtime_meta: dict[str, Any] | None,
|
| 868 |
+
helper_meta: dict[str, Any] | None,
|
| 869 |
+
result: Any,
|
| 870 |
+
) -> dict[str, Any]:
|
| 871 |
+
badges: list[dict[str, Any]] = []
|
| 872 |
+
row_count = _result_count(result)
|
| 873 |
+
if row_count is not None:
|
| 874 |
+
badges.append({"type": "Badge", "label": f"{row_count} rows", "variant": "outline"})
|
| 875 |
+
if isinstance(helper_meta, dict):
|
| 876 |
+
for key in ("total", "matched", "returned"):
|
| 877 |
+
if helper_meta.get(key) is not None:
|
| 878 |
+
badges.append(
|
| 879 |
+
{
|
| 880 |
+
"type": "Badge",
|
| 881 |
+
"label": f"{key}: {helper_meta[key]}",
|
| 882 |
+
"variant": "outline",
|
| 883 |
+
}
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
summary_children: list[dict[str, Any]] = [
|
| 887 |
+
{
|
| 888 |
+
"type": "CardHeader",
|
| 889 |
+
"children": [
|
| 890 |
+
{"type": "CardTitle", "content": "Hub search results"},
|
| 891 |
+
{"type": "CardDescription", "content": query},
|
| 892 |
+
],
|
| 893 |
+
}
|
| 894 |
+
]
|
| 895 |
+
|
| 896 |
+
if badges:
|
| 897 |
+
summary_children.append(
|
| 898 |
+
{
|
| 899 |
+
"type": "CardContent",
|
| 900 |
+
"children": [
|
| 901 |
+
{
|
| 902 |
+
"type": "Row",
|
| 903 |
+
"gap": 2,
|
| 904 |
+
"align": "center",
|
| 905 |
+
"cssClass": "flex-wrap",
|
| 906 |
+
"children": badges,
|
| 907 |
+
}
|
| 908 |
+
],
|
| 909 |
+
}
|
| 910 |
+
)
|
| 911 |
+
|
| 912 |
+
return {"type": "Card", "children": summary_children}
|
| 913 |
+
|
| 914 |
+
|
| 915 |
+
def build_runtime_wire(query: str, payload: dict[str, Any]) -> dict[str, Any]:
|
| 916 |
+
runtime_meta = payload.get("meta") if isinstance(payload.get("meta"), dict) else None
|
| 917 |
+
if isinstance(runtime_meta, dict) and runtime_meta.get("ok") is False:
|
| 918 |
+
return wrap_prefab_body(
|
| 919 |
+
extract_prefab_body_parts(error_wire(str(runtime_meta.get("error") or "Hub query failed")))[0],
|
| 920 |
+
meta=runtime_meta,
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
result = payload.get("result")
|
| 924 |
+
helper_meta: dict[str, Any] | None = None
|
| 925 |
+
body_children: list[dict[str, Any]] = []
|
| 926 |
+
|
| 927 |
+
if _looks_like_helper_envelope(result):
|
| 928 |
+
helper_meta = result.get("meta") if isinstance(result.get("meta"), dict) else None
|
| 929 |
+
if result.get("ok") is False:
|
| 930 |
+
message = str(result.get("error") or "Helper query failed")
|
| 931 |
+
body_children.append(
|
| 932 |
+
{
|
| 933 |
+
"type": "Alert",
|
| 934 |
+
"variant": "destructive",
|
| 935 |
+
"icon": "circle-x",
|
| 936 |
+
"children": [
|
| 937 |
+
{"type": "AlertTitle", "content": "Hub helper error"},
|
| 938 |
+
{"type": "AlertDescription", "content": message},
|
| 939 |
+
],
|
| 940 |
+
}
|
| 941 |
+
)
|
| 942 |
+
else:
|
| 943 |
+
helper_result: Any
|
| 944 |
+
items = result.get("items")
|
| 945 |
+
item = result.get("item")
|
| 946 |
+
if isinstance(items, list):
|
| 947 |
+
helper_result = items
|
| 948 |
+
elif item is not None:
|
| 949 |
+
helper_result = item
|
| 950 |
+
else:
|
| 951 |
+
helper_result = result
|
| 952 |
+
body_children.extend(_render_value("Results", helper_result))
|
| 953 |
+
else:
|
| 954 |
+
body_children.extend(_render_value("Results", result))
|
| 955 |
+
|
| 956 |
+
body_view = {
|
| 957 |
+
"type": "Column",
|
| 958 |
+
"gap": 6,
|
| 959 |
+
"cssClass": PAGE_CSS_CLASS,
|
| 960 |
+
"children": [
|
| 961 |
+
_build_summary_card(
|
| 962 |
+
query,
|
| 963 |
+
runtime_meta=runtime_meta,
|
| 964 |
+
helper_meta=helper_meta,
|
| 965 |
+
result=result,
|
| 966 |
+
),
|
| 967 |
+
*build_helper_sections(helper_meta),
|
| 968 |
+
*body_children,
|
| 969 |
+
],
|
| 970 |
+
}
|
| 971 |
+
return wrap_prefab_body(body_view, meta=runtime_meta)
|
| 972 |
+
|
| 973 |
+
|
| 974 |
+
def parse_direct_wire(text: str) -> dict[str, Any]:
|
| 975 |
+
return coerce_wire(parse_jsonish_value(text))
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
def parse_passthrough_wire(text: str) -> dict[str, Any]:
|
| 979 |
+
payload = parse_runtime_payload(text)
|
| 980 |
+
runtime_meta = payload.get("meta")
|
| 981 |
+
raw_wire = payload.get("result")
|
| 982 |
+
|
| 983 |
+
if raw_wire is None and isinstance(runtime_meta, dict):
|
| 984 |
+
error = runtime_meta.get("error")
|
| 985 |
+
if error:
|
| 986 |
+
body_view, state, defs, theme = extract_prefab_body_parts(error_wire(str(error)))
|
| 987 |
+
return wrap_prefab_body(
|
| 988 |
+
body_view,
|
| 989 |
+
meta=runtime_meta,
|
| 990 |
+
state=state,
|
| 991 |
+
defs=defs,
|
| 992 |
+
theme=theme,
|
| 993 |
+
)
|
| 994 |
+
|
| 995 |
+
if isinstance(raw_wire, (dict, list)):
|
| 996 |
+
try:
|
| 997 |
+
body_view, state, defs, theme = extract_prefab_body_parts(raw_wire)
|
| 998 |
+
except ValueError:
|
| 999 |
+
return build_runtime_wire("Hub query", payload)
|
| 1000 |
+
return wrap_prefab_body(
|
| 1001 |
+
body_view,
|
| 1002 |
+
meta=runtime_meta if isinstance(runtime_meta, dict) else None,
|
| 1003 |
+
state=state,
|
| 1004 |
+
defs=defs,
|
| 1005 |
+
theme=theme,
|
| 1006 |
+
)
|
| 1007 |
+
|
| 1008 |
+
return build_runtime_wire("Hub query", payload)
|