evalstate HF Staff commited on
Commit
8f0b0ef
·
verified ·
1 Parent(s): e57d3fe

Deploy gen-ui with pydantic-monty 0.0.17

Browse files
.prefab/tool-cards/monty_api_tool_v2.py CHANGED
@@ -18,13 +18,13 @@ HELPER_EXTERNALS = _MODULE.HELPER_EXTERNALS
18
  main = _MODULE.main
19
 
20
 
21
- async def hf_hub_query(
22
  query: str,
23
  code: str,
24
  max_calls: int | None = None,
25
  timeout_sec: int | None = None,
26
  ) -> dict[str, Any]:
27
- return await _MODULE.hf_hub_query(
28
  query=query,
29
  code=code,
30
  max_calls=max_calls,
@@ -32,13 +32,13 @@ async def hf_hub_query(
32
  )
33
 
34
 
35
- async def hf_hub_query_raw(
36
  query: str,
37
  code: str,
38
  max_calls: int | None = None,
39
  timeout_sec: int | None = None,
40
  ) -> Any:
41
- return await _MODULE.hf_hub_query_raw(
42
  query=query,
43
  code=code,
44
  max_calls=max_calls,
 
18
  main = _MODULE.main
19
 
20
 
21
+ def hf_hub_query(
22
  query: str,
23
  code: str,
24
  max_calls: int | None = None,
25
  timeout_sec: int | None = None,
26
  ) -> dict[str, Any]:
27
+ return _MODULE.hf_hub_query(
28
  query=query,
29
  code=code,
30
  max_calls=max_calls,
 
32
  )
33
 
34
 
35
+ def hf_hub_query_raw(
36
  query: str,
37
  code: str,
38
  max_calls: int | None = None,
39
  timeout_sec: int | None = None,
40
  ) -> Any:
41
+ return _MODULE.hf_hub_query_raw(
42
  query=query,
43
  code=code,
44
  max_calls=max_calls,
.prod/agent-cards/shared/_monty_codegen_shared.md CHANGED
@@ -3,24 +3,31 @@
3
  - You are writing Python to be executed in a secure runtime environment.
4
  - **NEVER** use `import` - it is NOT available in this environment.
5
  - All helper calls are async: always use `await`.
6
- - Use this exact outer shape:
7
 
8
  ```py
9
- async def solve(query, max_calls):
10
- ...
11
-
12
- await solve(query, max_calls)
13
  ```
14
 
 
15
  - `max_calls` is the total external-call budget for the whole program.
 
 
 
 
16
  - Use only documented `hf_*` helpers.
17
- - Return plain Python data only: `dict`, `list`, `str`, `int`, `float`, `bool`, or `None`.
18
- - Do **not** hand-build JSON strings or markdown strings inside `solve(...)` unless the user explicitly asked for prose.
19
- - Do **not** build your own transport wrapper like `{result: ..., meta: ...}`.
20
- - If the user says "return only" some fields, return exactly that final shape.
21
- - If a helper already returns the requested row shape, return `resp["items"]` directly **only when helper coverage is clearly complete**. If helper `meta` suggests partial/unknown coverage, return `{"results": resp["items"], "coverage": resp["meta"]}` instead of bare items.
22
  - For current-user prompts (`my`, `me`), try helpers with `username=None` / `handle=None` first.
23
- - If a current-user helper returns `ok=false`, return that helper response directly.
 
 
 
 
24
 
25
  ## Search rules
26
 
@@ -41,35 +48,46 @@ await solve(query, max_calls)
41
  - `hf_user_likes(...)` already returns full normalized like rows by default; omit `fields` unless the user asked for a subset.
42
  - When sorting `hf_user_likes(...)` by `repo_likes` or `repo_downloads`, set `ranking_window=50` unless the user explicitly asked for a narrower recent window.
43
  - For human-facing follower/member/liker lists without an explicit requested count, prefer `limit=100` and return coverage when more may exist.
 
44
  - Unknown `fields` / `where` keys now fail fast. Use only canonical field names.
45
-
46
- - Ownership phrasing like "what collections does Qwen have", "collections by Qwen", or "collections owned by Qwen" means an owner lookup, so use `hf_collections_search(owner="Qwen")`, not a keyword-only `query="Qwen"` search.
47
  - Ownership phrasing like "what spaces does X have", "what models does X have", or "what datasets does X have" means an author/owner inventory lookup, so use `hf_spaces_search(author="X")`, `hf_models_search(author="X")`, or `hf_datasets_search(author="X")` rather than a global keyword-only search.
48
- - Owner/user/org handles may arrive with different casing in the user message; when a handle spelling is uncertain, prefer owner-oriented logic and, if needed, add fallback inside `solve(...)` that broadens to `query=...` and filters owners case-insensitively.
 
 
49
  - For exact aggregate counts like "how many models/datasets/spaces does X have", prefer `hf_profile_summary(...)['item']` counts. Those overview-owned counts may differ slightly from visible public search/list results, so if the user also asked for the list, preserve that distinction.
50
  - For owner inventory queries without an explicit requested count, use `hf_profile_summary(...)` first when a specific owner is known. If the count is modest, use it to size the follow-up list call; otherwise return a bounded list plus coverage instead of pretending completeness.
51
  - Think like `huggingface_hub`: `search`, `filter`, `author`, repo-type-specific upstream params, then `fields`.
52
  - Push constraints upstream whenever a first-class helper argument exists.
53
  - `post_filter` is only for normalized row filters that cannot be pushed upstream.
 
 
54
  - Keep `post_filter` simple:
55
  - exact match or `in` for returned fields like `runtime_stage`
56
- - `gte` / `lte` for normalized numeric fields like `num_params`, `downloads`, and `likes`
57
- - `num_params` is one of the main valid reasons to use `post_filter` on model search today.
58
- - Do **not** use `post_filter` for things that already have first-class upstream params like `author`, `pipeline_tag`, `dataset_name`, `language`, `models`, or `datasets`.
59
 
60
  Examples:
61
 
62
  ```py
63
- await hf_models_search(pipeline_tag="text-to-image", limit=10)
64
- await hf_datasets_search(search="speech", sort="downloads", limit=10)
65
- await hf_spaces_search(post_filter={"runtime_stage": {"in": ["BUILD_ERROR", "RUNTIME_ERROR"]}})
66
- await hf_models_search(
 
 
67
  pipeline_tag="text-generation",
 
68
  sort="trending_score",
69
  limit=50,
70
- post_filter={"num_params": {"gte": 20_000_000_000, "lte": 80_000_000_000}},
71
  )
72
- await hf_collections_search(owner="Qwen", limit=10)
 
 
 
 
 
73
  ```
74
 
75
  Field-only pattern:
@@ -80,7 +98,8 @@ resp = await hf_models_search(
80
  fields=["repo_id", "author", "likes", "downloads", "repo_url"],
81
  limit=3,
82
  )
83
- return resp["items"]
 
84
  ```
85
 
86
  Coverage pattern:
@@ -93,7 +112,8 @@ resp = await hf_user_likes(
93
  limit=20,
94
  fields=["repo_id", "repo_likes", "repo_url"],
95
  )
96
- return {"results": resp["items"], "coverage": resp["meta"]}
 
97
  ```
98
 
99
  Owner-inventory pattern:
@@ -109,31 +129,133 @@ resp = await hf_spaces_search(
109
  )
110
  meta = resp.get("meta") or {}
111
  if meta.get("limit_boundary_hit") or meta.get("more_available") not in {False, None}:
112
- return {"results": resp["items"], "coverage": {**meta, "profile_spaces_count": count}}
113
- return resp["items"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  ```
115
 
116
- Profile-count pattern:
117
 
118
  ```py
119
- profile = await hf_profile_summary(handle="mishig")
120
- item = profile["item"] or {}
121
- return {
122
- "followers_count": item.get("followers_count"),
123
- "following_count": item.get("following_count"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
 
 
125
  ```
126
 
127
- Pro-followers pattern:
128
 
129
  ```py
130
- followers = await hf_user_graph(
131
  relation="followers",
132
  pro_only=True,
133
- limit=20,
134
  fields=["username"],
135
  )
136
- return followers["items"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  ```
138
 
139
  ## Navigation graph
@@ -150,7 +272,7 @@ Use the helper that matches the question type.
150
  - repo discussions → `hf_repo_discussions(...)`
151
  - specific discussion details → `hf_repo_discussion_details(...)`
152
  - users who liked one repo → `hf_repo_likers(...)`
153
- - profile / overview / aggregate counts → `hf_profile_summary(...)`
154
  - followers / following lists → `hf_user_graph(...)`
155
  - repos a user liked → `hf_user_likes(...)`
156
  - recent activity feed → `hf_recent_activity(...)`
@@ -182,16 +304,12 @@ Rules:
182
  - `items` is the canonical list field.
183
  - `item` is just a singleton convenience.
184
  - `meta` contains helper-owned execution, limit, and coverage info.
185
- - When helper-owned coverage matters, prefer returning the helper envelope directly.
186
 
187
  ## High-signal output rules
188
 
189
  - Prefer compact dict/list outputs over prose when the user asked for fields.
190
- - Prefer summary helpers before detail hydration.
191
  - Use canonical snake_case keys in generated code and structured output.
192
  - Use `repo_id` as the display label for repos.
193
- - Use `hf_profile_summary(...)['item']` for aggregate counts such as followers, following, models, datasets, and spaces.
194
- - For selective one-shot search helpers, treat `meta.limit_boundary_hit=true` as a partial/unknown-coverage warning even if `meta.truncated` is still `false`.
195
  - For joins/intersections/rankings, fetch the needed working set first and compute locally.
196
  - If the result is partial, use top-level keys `results` and `coverage`.
197
 
@@ -207,9 +325,9 @@ await hf_collections_search(query: 'str | None' = None, owner: 'str | None' = No
207
 
208
  await hf_daily_papers(limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
209
 
210
- await hf_datasets_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, benchmark: 'str | bool | None' = None, dataset_name: 'str | None' = None, gated: 'bool | None' = None, language_creators: 'str | list[str] | None' = None, language: 'str | list[str] | None' = None, multilinguality: 'str | list[str] | None' = None, size_categories: 'str | list[str] | None' = None, task_categories: 'str | list[str] | None' = None, task_ids: 'str | list[str] | None' = None, sort: 'str | None' = None, limit: 'int' = 20, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
211
 
212
- await hf_models_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, apps: 'str | list[str] | None' = None, gated: 'bool | None' = None, inference: 'str | None' = None, inference_provider: 'str | list[str] | None' = None, model_name: 'str | None' = None, trained_dataset: 'str | list[str] | None' = None, pipeline_tag: 'str | None' = None, emissions_thresholds: 'tuple[float, float] | None' = None, sort: 'str | None' = None, limit: 'int' = 20, expand: 'list[str] | None' = None, full: 'bool | None' = None, card_data: 'bool' = False, fetch_config: 'bool' = False, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
213
 
214
  await hf_org_members(organization: 'str', limit: 'int | None' = None, scan_limit: 'int | None' = None, count_only: 'bool' = False, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
215
 
@@ -225,11 +343,11 @@ await hf_repo_discussions(repo_type: 'str', repo_id: 'str', limit: 'int' = 20, f
225
 
226
  await hf_repo_likers(repo_id: 'str', repo_type: 'str', limit: 'int | None' = None, count_only: 'bool' = False, pro_only: 'bool | None' = None, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
227
 
228
- await hf_repo_search(search: 'str | None' = None, repo_type: 'str | None' = None, repo_types: 'list[str] | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, sort: 'str | None' = None, limit: 'int' = 20, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
229
 
230
  await hf_runtime_capabilities(section: 'str | None' = None) -> 'dict[str, Any]'
231
 
232
- await hf_spaces_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, datasets: 'str | list[str] | None' = None, models: 'str | list[str] | None' = None, linked: 'bool' = False, sort: 'str | None' = None, limit: 'int' = 20, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
233
 
234
  await hf_trending(repo_type: 'str' = 'model', limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
235
 
@@ -336,7 +454,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
336
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
337
  - normalized_only: `true`
338
  - limit_contract:
339
- - default_limit: `20`
340
  - max_limit: `5000`
341
  - notes: Thin dataset-search wrapper around the Hub list_datasets path. Prefer this over hf_repo_search for dataset-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
342
 
@@ -350,7 +468,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
350
  - default_fields: `repo_id`, `repo_type`, `author`, `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `repo_url`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
351
  - guaranteed_fields: `repo_id`, `repo_type`, `author`, `repo_url`
352
  - optional_fields: `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
353
- - supported_params: `search`, `filter`, `author`, `apps`, `gated`, `inference`, `inference_provider`, `model_name`, `trained_dataset`, `pipeline_tag`, `emissions_thresholds`, `sort`, `limit`, `expand`, `full`, `card_data`, `fetch_config`, `fields`, `post_filter`
354
  - sort_values: `created_at`, `downloads`, `last_modified`, `likes`, `trending_score`
355
  - expand_values: `author`, `base_models`, `card_data`, `config`, `created_at`, `disabled`, `downloads`, `downloads_all_time`, `eval_results`, `gated`, `gguf`, `inference`, `inference_provider_mapping`, `last_modified`, `library_name`, `likes`, `mask_token`, `model_index`, `pipeline_tag`, `private`, `resource_group`, `safetensors`, `sha`, `siblings`, `spaces`, `tags`, `transformers_info`, `trending_score`, `widget_data`, `xet_enabled`, `gitaly_uid`
356
  - fields_contract:
@@ -361,7 +479,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
361
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
362
  - normalized_only: `true`
363
  - limit_contract:
364
- - default_limit: `20`
365
  - max_limit: `5000`
366
  - notes: Thin model-search wrapper around the Hub list_models path. Prefer this over hf_repo_search for model-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
367
 
@@ -532,7 +650,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
532
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
533
  - normalized_only: `true`
534
  - limit_contract:
535
- - default_limit: `20`
536
  - max_limit: `5000`
537
  - notes: Small generic repo-search helper. Prefer hf_models_search, hf_datasets_search, or hf_spaces_search for single-type queries; use hf_repo_search for intentionally cross-type search. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
538
 
@@ -571,7 +689,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
571
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
572
  - normalized_only: `true`
573
  - limit_contract:
574
- - default_limit: `20`
575
  - max_limit: `5000`
576
  - notes: Thin space-search wrapper around the Hub list_spaces path. Prefer this over hf_repo_search for space-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
577
 
 
3
  - You are writing Python to be executed in a secure runtime environment.
4
  - **NEVER** use `import` - it is NOT available in this environment.
5
  - All helper calls are async: always use `await`.
6
+ - Write a top-level Monty Python script. Use a shape like:
7
 
8
  ```py
9
+ resp = await hf_models_search(limit=min(max_calls, 10))
10
+ result = resp["items"]
11
+ result
 
12
  ```
13
 
14
+ - `max_calls` is a runtime-provided top-level input.
15
  - `max_calls` is the total external-call budget for the whole program.
16
+ - Always assign the final output to `result`.
17
+ - End the script with a final line containing only `result`.
18
+ - Never stop after `result = ...`; always add a final bare `result` line.
19
+ - Do **not** define or call `solve(...)`.
20
  - Use only documented `hf_*` helpers.
21
+ - `result` must be plain Python data only: `dict`, `list`, `str`, `int`, `float`, `bool`, or `None`.
22
+ - Do **not** hand-build JSON strings, markdown strings, or your own transport wrapper like `{result: ..., meta: ...}` unless the user explicitly asked for prose.
23
+ - If the user says "return only" some fields, make `result` exactly that shape.
24
+ - If a helper already returns the requested row shape, use `resp["items"]` directly **only when helper coverage is clearly complete**. If helper `meta` suggests partial/unknown coverage, set `result = {"results": resp["items"], "coverage": resp["meta"]}` instead of bare items.
 
25
  - For current-user prompts (`my`, `me`), try helpers with `username=None` / `handle=None` first.
26
+ - For current-user follower/following aggregation prompts, prefer `hf_user_graph(relation=..., ...)` directly instead of `hf_whoami()` plus a second graph call. This saves a call and avoids unnecessary branching.
27
+ - If a current-user helper returns `ok=false`, assign that helper response to `result`.
28
+ - For relationship / aggregation questions (followers, members, likes, likers, intersections), preserve attribution in `result` unless the user explicitly asked for a collapsed deduped list.
29
+ - Do **not** choose tiny hard-coded limits like `5` for follower/member/likes aggregation unless the user explicitly asked for a tiny sample. Prefer larger limits and preserve coverage when partial.
30
+ - If you branch on an error path, you must still end the module with a final top-level bare `result` line outside every `if` / loop.
31
 
32
  ## Search rules
33
 
 
48
  - `hf_user_likes(...)` already returns full normalized like rows by default; omit `fields` unless the user asked for a subset.
49
  - When sorting `hf_user_likes(...)` by `repo_likes` or `repo_downloads`, set `ranking_window=50` unless the user explicitly asked for a narrower recent window.
50
  - For human-facing follower/member/liker lists without an explicit requested count, prefer `limit=100` and return coverage when more may exist.
51
+ - For follower/following/member/liker queries that require local filtering on actor fields such as `username` or `fullname`, prefer a bounded scan like `limit=100` / `scan_limit=100` by default, or at most about `200` when a slightly broader sample is justified. Do **not** jump to `1000` unless the user explicitly asked for exhaustive coverage or a very large sample.
52
  - Unknown `fields` / `where` keys now fail fast. Use only canonical field names.
53
+ - Ownership phrasing like "what collections does Qwen have", "collections by Qwen", or "collections owned by Qwen" means an owner lookup, so use `hf_collections_search(owner="Qwen")`, not a keyword-only `query="Qwen"` search; it filters owners case-insensitively.
 
54
  - Ownership phrasing like "what spaces does X have", "what models does X have", or "what datasets does X have" means an author/owner inventory lookup, so use `hf_spaces_search(author="X")`, `hf_models_search(author="X")`, or `hf_datasets_search(author="X")` rather than a global keyword-only search.
55
+ - For profile/detail/social questions about a user or org bio, description, display name, website, GitHub, Twitter/X, LinkedIn, Bluesky, organizations, or pro status — use `hf_profile_summary(...)` first.
56
+ - For join-style questions that need profile details for followers, following, members, likers, or other actor lists, first fetch a **bounded** actor list, filter locally on actor fields like `username` / `fullname`, then hydrate only the bounded matches with `hf_profile_summary(...)`.
57
+ - Do **not** set the initial actor-list limit equal to the whole remaining call budget when each match needs a follow-up profile lookup; reserve budget for the profile-detail calls and return coverage if the hydration step is partial.
58
  - For exact aggregate counts like "how many models/datasets/spaces does X have", prefer `hf_profile_summary(...)['item']` counts. Those overview-owned counts may differ slightly from visible public search/list results, so if the user also asked for the list, preserve that distinction.
59
  - For owner inventory queries without an explicit requested count, use `hf_profile_summary(...)` first when a specific owner is known. If the count is modest, use it to size the follow-up list call; otherwise return a bounded list plus coverage instead of pretending completeness.
60
  - Think like `huggingface_hub`: `search`, `filter`, `author`, repo-type-specific upstream params, then `fields`.
61
  - Push constraints upstream whenever a first-class helper argument exists.
62
  - `post_filter` is only for normalized row filters that cannot be pushed upstream.
63
+ - `num_params` is a first-class upstream model-search arg; use `num_params="min:6B,max:128B"` instead of `post_filter` when possible.
64
+ - For created/updated date constraints, pair local `post_filter` with the matching sort (`created_at` or `last_modified`). Do **not** rely on date-only `post_filter` over an unsorted repo search window.
65
  - Keep `post_filter` simple:
66
  - exact match or `in` for returned fields like `runtime_stage`
67
+ - `gte` / `lte` for normalized numeric fields like `downloads` and `likes`
68
+ - `gte` / `lte` also work for normalized ISO timestamp fields like `created_at` and `last_modified`
69
+ - Do **not** use `post_filter` for things that already have first-class upstream params like `author`, `pipeline_tag`, `num_params` on model search, `dataset_name`, `language`, `models`, or `datasets`.
70
 
71
  Examples:
72
 
73
  ```py
74
+ result = await hf_models_search(pipeline_tag="text-to-image", limit=10)
75
+ result
76
+ ```
77
+
78
+ ```py
79
+ result = await hf_models_search(
80
  pipeline_tag="text-generation",
81
+ num_params="min:20B,max:80B",
82
  sort="trending_score",
83
  limit=50,
 
84
  )
85
+ result
86
+ ```
87
+
88
+ ```py
89
+ result = await hf_collections_search(owner="Qwen", limit=10)
90
+ result
91
  ```
92
 
93
  Field-only pattern:
 
98
  fields=["repo_id", "author", "likes", "downloads", "repo_url"],
99
  limit=3,
100
  )
101
+ result = resp["items"]
102
+ result
103
  ```
104
 
105
  Coverage pattern:
 
112
  limit=20,
113
  fields=["repo_id", "repo_likes", "repo_url"],
114
  )
115
+ result = {"results": resp["items"], "coverage": resp["meta"]}
116
+ result
117
  ```
118
 
119
  Owner-inventory pattern:
 
129
  )
130
  meta = resp.get("meta") or {}
131
  if meta.get("limit_boundary_hit") or meta.get("more_available") not in {False, None}:
132
+ result = {"results": resp["items"], "coverage": {**meta, "profile_spaces_count": count}}
133
+ else:
134
+ result = resp["items"]
135
+ result
136
+ ```
137
+
138
+ Follower-profile join pattern:
139
+
140
+ ```py
141
+ followers_resp = await hf_user_graph(
142
+ relation="followers",
143
+ limit=100,
144
+ scan_limit=100,
145
+ fields=["username", "fullname"],
146
+ )
147
+ followers = followers_resp.get("items") or []
148
+ matches = []
149
+ for follower in followers:
150
+ username = follower.get("username")
151
+ fullname = follower.get("fullname")
152
+ starts_with_b = (
153
+ (isinstance(username, str) and username.lower().startswith("b"))
154
+ or (isinstance(fullname, str) and fullname.lower().startswith("b"))
155
+ )
156
+ if starts_with_b:
157
+ matches.append(follower)
158
+ remaining_profile_calls = max(0, max_calls - 1)
159
+ results = []
160
+ for follower in matches[:remaining_profile_calls]:
161
+ username = follower.get("username")
162
+ if not username:
163
+ continue
164
+ profile = await hf_profile_summary(handle=username)
165
+ item = profile.get("item") or {}
166
+ results.append(
167
+ {
168
+ "username": username,
169
+ "fullname": follower.get("fullname"),
170
+ "github_url": item.get("github_url"),
171
+ }
172
+ )
173
+ result = {
174
+ "results": results,
175
+ "coverage": {
176
+ "followers": followers_resp.get("meta") or {},
177
+ "matching_followers_seen": len(matches),
178
+ "profile_calls_used": len(results),
179
+ "profile_hydration_partial": len(matches) > len(results),
180
+ },
181
+ }
182
+ result
183
  ```
184
 
185
+ Follower-likes aggregation pattern:
186
 
187
  ```py
188
+ followers_resp = await hf_user_graph(relation="followers", limit=100, fields=["username"])
189
+ followers = followers_resp.get("items") or []
190
+ results = []
191
+ for follower in followers:
192
+ username = follower.get("username")
193
+ if not username:
194
+ continue
195
+ likes_resp = await hf_user_likes(
196
+ username=username,
197
+ repo_types=["model"],
198
+ limit=20,
199
+ fields=["repo_id", "liked_at"],
200
+ )
201
+ results.append(
202
+ {
203
+ "follower": username,
204
+ "liked_models": likes_resp.get("items") or [],
205
+ }
206
+ )
207
+ coverage = {
208
+ "followers": followers_resp.get("meta") or {},
209
  }
210
+ result = {"results": results, "coverage": coverage}
211
+ result
212
  ```
213
 
214
+ Current-user pro-follower model-likes pattern:
215
 
216
  ```py
217
+ followers_resp = await hf_user_graph(
218
  relation="followers",
219
  pro_only=True,
220
+ limit=100,
221
  fields=["username"],
222
  )
223
+ followers = followers_resp.get("items") or []
224
+ remaining_calls = max(0, max_calls - 1)
225
+ results = {}
226
+ partial = (
227
+ (followers_resp.get("meta") or {}).get("limit_boundary_hit")
228
+ or (followers_resp.get("meta") or {}).get("more_available") not in {False, None}
229
+ )
230
+ processed_followers = 0
231
+ for follower in followers:
232
+ if remaining_calls <= 0:
233
+ partial = True
234
+ break
235
+ username = follower.get("username")
236
+ if not username:
237
+ continue
238
+ likes_resp = await hf_user_likes(
239
+ username=username,
240
+ repo_types=["model"],
241
+ limit=2,
242
+ fields=["repo_id", "repo_author", "liked_at"],
243
+ )
244
+ remaining_calls -= 1
245
+ likes_meta = likes_resp.get("meta") or {}
246
+ if likes_meta.get("limit_boundary_hit") or likes_meta.get("more_available") not in {False, None}:
247
+ partial = True
248
+ items = likes_resp.get("items") or []
249
+ if items:
250
+ results[username] = items
251
+ processed_followers += 1
252
+ coverage = {
253
+ "followers": followers_resp.get("meta") or {},
254
+ "processed_followers": processed_followers,
255
+ "partial": partial,
256
+ }
257
+ result = {"results": results, "coverage": coverage}
258
+ result
259
  ```
260
 
261
  ## Navigation graph
 
272
  - repo discussions → `hf_repo_discussions(...)`
273
  - specific discussion details → `hf_repo_discussion_details(...)`
274
  - users who liked one repo → `hf_repo_likers(...)`
275
+ - profile / overview / social/detail / aggregate counts → `hf_profile_summary(...)`
276
  - followers / following lists → `hf_user_graph(...)`
277
  - repos a user liked → `hf_user_likes(...)`
278
  - recent activity feed → `hf_recent_activity(...)`
 
304
  - `items` is the canonical list field.
305
  - `item` is just a singleton convenience.
306
  - `meta` contains helper-owned execution, limit, and coverage info.
 
307
 
308
  ## High-signal output rules
309
 
310
  - Prefer compact dict/list outputs over prose when the user asked for fields.
 
311
  - Use canonical snake_case keys in generated code and structured output.
312
  - Use `repo_id` as the display label for repos.
 
 
313
  - For joins/intersections/rankings, fetch the needed working set first and compute locally.
314
  - If the result is partial, use top-level keys `results` and `coverage`.
315
 
 
325
 
326
  await hf_daily_papers(limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
327
 
328
+ await hf_datasets_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, benchmark: 'str | bool | None' = None, dataset_name: 'str | None' = None, gated: 'bool | None' = None, language_creators: 'str | list[str] | None' = None, language: 'str | list[str] | None' = None, multilinguality: 'str | list[str] | None' = None, size_categories: 'str | list[str] | None' = None, task_categories: 'str | list[str] | None' = None, task_ids: 'str | list[str] | None' = None, sort: 'str | None' = None, limit: 'int' = 100, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
329
 
330
+ await hf_models_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, apps: 'str | list[str] | None' = None, gated: 'bool | None' = None, inference: 'str | None' = None, inference_provider: 'str | list[str] | None' = None, model_name: 'str | None' = None, trained_dataset: 'str | list[str] | None' = None, pipeline_tag: 'str | None' = None, num_params: 'str | None' = None, emissions_thresholds: 'tuple[float, float] | None' = None, sort: 'str | None' = None, limit: 'int' = 100, expand: 'list[str] | None' = None, full: 'bool | None' = None, card_data: 'bool' = False, fetch_config: 'bool' = False, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
331
 
332
  await hf_org_members(organization: 'str', limit: 'int | None' = None, scan_limit: 'int | None' = None, count_only: 'bool' = False, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
333
 
 
343
 
344
  await hf_repo_likers(repo_id: 'str', repo_type: 'str', limit: 'int | None' = None, count_only: 'bool' = False, pro_only: 'bool | None' = None, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
345
 
346
+ await hf_repo_search(search: 'str | None' = None, repo_type: 'str | None' = None, repo_types: 'list[str] | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, sort: 'str | None' = None, limit: 'int' = 100, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
347
 
348
  await hf_runtime_capabilities(section: 'str | None' = None) -> 'dict[str, Any]'
349
 
350
+ await hf_spaces_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, datasets: 'str | list[str] | None' = None, models: 'str | list[str] | None' = None, linked: 'bool' = False, sort: 'str | None' = None, limit: 'int' = 100, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
351
 
352
  await hf_trending(repo_type: 'str' = 'model', limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
353
 
 
454
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
455
  - normalized_only: `true`
456
  - limit_contract:
457
+ - default_limit: `100`
458
  - max_limit: `5000`
459
  - notes: Thin dataset-search wrapper around the Hub list_datasets path. Prefer this over hf_repo_search for dataset-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
460
 
 
468
  - default_fields: `repo_id`, `repo_type`, `author`, `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `repo_url`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
469
  - guaranteed_fields: `repo_id`, `repo_type`, `author`, `repo_url`
470
  - optional_fields: `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
471
+ - supported_params: `search`, `filter`, `author`, `apps`, `gated`, `inference`, `inference_provider`, `model_name`, `trained_dataset`, `pipeline_tag`, `num_params`, `emissions_thresholds`, `sort`, `limit`, `expand`, `full`, `card_data`, `fetch_config`, `fields`, `post_filter`
472
  - sort_values: `created_at`, `downloads`, `last_modified`, `likes`, `trending_score`
473
  - expand_values: `author`, `base_models`, `card_data`, `config`, `created_at`, `disabled`, `downloads`, `downloads_all_time`, `eval_results`, `gated`, `gguf`, `inference`, `inference_provider_mapping`, `last_modified`, `library_name`, `likes`, `mask_token`, `model_index`, `pipeline_tag`, `private`, `resource_group`, `safetensors`, `sha`, `siblings`, `spaces`, `tags`, `transformers_info`, `trending_score`, `widget_data`, `xet_enabled`, `gitaly_uid`
474
  - fields_contract:
 
479
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
480
  - normalized_only: `true`
481
  - limit_contract:
482
+ - default_limit: `100`
483
  - max_limit: `5000`
484
  - notes: Thin model-search wrapper around the Hub list_models path. Prefer this over hf_repo_search for model-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
485
 
 
650
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
651
  - normalized_only: `true`
652
  - limit_contract:
653
+ - default_limit: `100`
654
  - max_limit: `5000`
655
  - notes: Small generic repo-search helper. Prefer hf_models_search, hf_datasets_search, or hf_spaces_search for single-type queries; use hf_repo_search for intentionally cross-type search. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
656
 
 
689
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
690
  - normalized_only: `true`
691
  - limit_contract:
692
+ - default_limit: `100`
693
  - max_limit: `5000`
694
  - notes: Thin space-search wrapper around the Hub list_spaces path. Prefer this over hf_repo_search for space-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
695
 
.prod/agent-cards/shared/_monty_codegen_shared.template.md CHANGED
@@ -3,24 +3,31 @@
3
  - You are writing Python to be executed in a secure runtime environment.
4
  - **NEVER** use `import` - it is NOT available in this environment.
5
  - All helper calls are async: always use `await`.
6
- - Use this exact outer shape:
7
 
8
  ```py
9
- async def solve(query, max_calls):
10
- ...
11
-
12
- await solve(query, max_calls)
13
  ```
14
 
 
15
  - `max_calls` is the total external-call budget for the whole program.
 
 
 
 
16
  - Use only documented `hf_*` helpers.
17
- - Return plain Python data only: `dict`, `list`, `str`, `int`, `float`, `bool`, or `None`.
18
- - Do **not** hand-build JSON strings or markdown strings inside `solve(...)` unless the user explicitly asked for prose.
19
- - Do **not** build your own transport wrapper like `{result: ..., meta: ...}`.
20
- - If the user says "return only" some fields, return exactly that final shape.
21
- - If a helper already returns the requested row shape, return `resp["items"]` directly **only when helper coverage is clearly complete**. If helper `meta` suggests partial/unknown coverage, return `{"results": resp["items"], "coverage": resp["meta"]}` instead of bare items.
22
  - For current-user prompts (`my`, `me`), try helpers with `username=None` / `handle=None` first.
23
- - If a current-user helper returns `ok=false`, return that helper response directly.
 
 
 
 
24
 
25
  ## Search rules
26
 
@@ -41,35 +48,46 @@ await solve(query, max_calls)
41
  - `hf_user_likes(...)` already returns full normalized like rows by default; omit `fields` unless the user asked for a subset.
42
  - When sorting `hf_user_likes(...)` by `repo_likes` or `repo_downloads`, set `ranking_window=50` unless the user explicitly asked for a narrower recent window.
43
  - For human-facing follower/member/liker lists without an explicit requested count, prefer `limit=100` and return coverage when more may exist.
 
44
  - Unknown `fields` / `where` keys now fail fast. Use only canonical field names.
45
-
46
- - Ownership phrasing like "what collections does Qwen have", "collections by Qwen", or "collections owned by Qwen" means an owner lookup, so use `hf_collections_search(owner="Qwen")`, not a keyword-only `query="Qwen"` search.
47
  - Ownership phrasing like "what spaces does X have", "what models does X have", or "what datasets does X have" means an author/owner inventory lookup, so use `hf_spaces_search(author="X")`, `hf_models_search(author="X")`, or `hf_datasets_search(author="X")` rather than a global keyword-only search.
48
- - Owner/user/org handles may arrive with different casing in the user message; when a handle spelling is uncertain, prefer owner-oriented logic and, if needed, add fallback inside `solve(...)` that broadens to `query=...` and filters owners case-insensitively.
 
 
49
  - For exact aggregate counts like "how many models/datasets/spaces does X have", prefer `hf_profile_summary(...)['item']` counts. Those overview-owned counts may differ slightly from visible public search/list results, so if the user also asked for the list, preserve that distinction.
50
  - For owner inventory queries without an explicit requested count, use `hf_profile_summary(...)` first when a specific owner is known. If the count is modest, use it to size the follow-up list call; otherwise return a bounded list plus coverage instead of pretending completeness.
51
  - Think like `huggingface_hub`: `search`, `filter`, `author`, repo-type-specific upstream params, then `fields`.
52
  - Push constraints upstream whenever a first-class helper argument exists.
53
  - `post_filter` is only for normalized row filters that cannot be pushed upstream.
 
 
54
  - Keep `post_filter` simple:
55
  - exact match or `in` for returned fields like `runtime_stage`
56
- - `gte` / `lte` for normalized numeric fields like `num_params`, `downloads`, and `likes`
57
- - `num_params` is one of the main valid reasons to use `post_filter` on model search today.
58
- - Do **not** use `post_filter` for things that already have first-class upstream params like `author`, `pipeline_tag`, `dataset_name`, `language`, `models`, or `datasets`.
59
 
60
  Examples:
61
 
62
  ```py
63
- await hf_models_search(pipeline_tag="text-to-image", limit=10)
64
- await hf_datasets_search(search="speech", sort="downloads", limit=10)
65
- await hf_spaces_search(post_filter={"runtime_stage": {"in": ["BUILD_ERROR", "RUNTIME_ERROR"]}})
66
- await hf_models_search(
 
 
67
  pipeline_tag="text-generation",
 
68
  sort="trending_score",
69
  limit=50,
70
- post_filter={"num_params": {"gte": 20_000_000_000, "lte": 80_000_000_000}},
71
  )
72
- await hf_collections_search(owner="Qwen", limit=10)
 
 
 
 
 
73
  ```
74
 
75
  Field-only pattern:
@@ -80,7 +98,8 @@ resp = await hf_models_search(
80
  fields=["repo_id", "author", "likes", "downloads", "repo_url"],
81
  limit=3,
82
  )
83
- return resp["items"]
 
84
  ```
85
 
86
  Coverage pattern:
@@ -93,7 +112,8 @@ resp = await hf_user_likes(
93
  limit=20,
94
  fields=["repo_id", "repo_likes", "repo_url"],
95
  )
96
- return {"results": resp["items"], "coverage": resp["meta"]}
 
97
  ```
98
 
99
  Owner-inventory pattern:
@@ -109,31 +129,133 @@ resp = await hf_spaces_search(
109
  )
110
  meta = resp.get("meta") or {}
111
  if meta.get("limit_boundary_hit") or meta.get("more_available") not in {False, None}:
112
- return {"results": resp["items"], "coverage": {**meta, "profile_spaces_count": count}}
113
- return resp["items"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  ```
115
 
116
- Profile-count pattern:
117
 
118
  ```py
119
- profile = await hf_profile_summary(handle="mishig")
120
- item = profile["item"] or {}
121
- return {
122
- "followers_count": item.get("followers_count"),
123
- "following_count": item.get("following_count"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
 
 
125
  ```
126
 
127
- Pro-followers pattern:
128
 
129
  ```py
130
- followers = await hf_user_graph(
131
  relation="followers",
132
  pro_only=True,
133
- limit=20,
134
  fields=["username"],
135
  )
136
- return followers["items"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  ```
138
 
139
  ## Navigation graph
@@ -150,7 +272,7 @@ Use the helper that matches the question type.
150
  - repo discussions → `hf_repo_discussions(...)`
151
  - specific discussion details → `hf_repo_discussion_details(...)`
152
  - users who liked one repo → `hf_repo_likers(...)`
153
- - profile / overview / aggregate counts → `hf_profile_summary(...)`
154
  - followers / following lists → `hf_user_graph(...)`
155
  - repos a user liked → `hf_user_likes(...)`
156
  - recent activity feed → `hf_recent_activity(...)`
@@ -182,16 +304,12 @@ Rules:
182
  - `items` is the canonical list field.
183
  - `item` is just a singleton convenience.
184
  - `meta` contains helper-owned execution, limit, and coverage info.
185
- - When helper-owned coverage matters, prefer returning the helper envelope directly.
186
 
187
  ## High-signal output rules
188
 
189
  - Prefer compact dict/list outputs over prose when the user asked for fields.
190
- - Prefer summary helpers before detail hydration.
191
  - Use canonical snake_case keys in generated code and structured output.
192
  - Use `repo_id` as the display label for repos.
193
- - Use `hf_profile_summary(...)['item']` for aggregate counts such as followers, following, models, datasets, and spaces.
194
- - For selective one-shot search helpers, treat `meta.limit_boundary_hit=true` as a partial/unknown-coverage warning even if `meta.truncated` is still `false`.
195
  - For joins/intersections/rankings, fetch the needed working set first and compute locally.
196
  - If the result is partial, use top-level keys `results` and `coverage`.
197
 
 
3
  - You are writing Python to be executed in a secure runtime environment.
4
  - **NEVER** use `import` - it is NOT available in this environment.
5
  - All helper calls are async: always use `await`.
6
+ - Write a top-level Monty Python script. Use a shape like:
7
 
8
  ```py
9
+ resp = await hf_models_search(limit=min(max_calls, 10))
10
+ result = resp["items"]
11
+ result
 
12
  ```
13
 
14
+ - `max_calls` is a runtime-provided top-level input.
15
  - `max_calls` is the total external-call budget for the whole program.
16
+ - Always assign the final output to `result`.
17
+ - End the script with a final line containing only `result`.
18
+ - Never stop after `result = ...`; always add a final bare `result` line.
19
+ - Do **not** define or call `solve(...)`.
20
  - Use only documented `hf_*` helpers.
21
+ - `result` must be plain Python data only: `dict`, `list`, `str`, `int`, `float`, `bool`, or `None`.
22
+ - Do **not** hand-build JSON strings, markdown strings, or your own transport wrapper like `{result: ..., meta: ...}` unless the user explicitly asked for prose.
23
+ - If the user says "return only" some fields, make `result` exactly that shape.
24
+ - If a helper already returns the requested row shape, use `resp["items"]` directly **only when helper coverage is clearly complete**. If helper `meta` suggests partial/unknown coverage, set `result = {"results": resp["items"], "coverage": resp["meta"]}` instead of bare items.
 
25
  - For current-user prompts (`my`, `me`), try helpers with `username=None` / `handle=None` first.
26
+ - For current-user follower/following aggregation prompts, prefer `hf_user_graph(relation=..., ...)` directly instead of `hf_whoami()` plus a second graph call. This saves a call and avoids unnecessary branching.
27
+ - If a current-user helper returns `ok=false`, assign that helper response to `result`.
28
+ - For relationship / aggregation questions (followers, members, likes, likers, intersections), preserve attribution in `result` unless the user explicitly asked for a collapsed deduped list.
29
+ - Do **not** choose tiny hard-coded limits like `5` for follower/member/likes aggregation unless the user explicitly asked for a tiny sample. Prefer larger limits and preserve coverage when partial.
30
+ - If you branch on an error path, you must still end the module with a final top-level bare `result` line outside every `if` / loop.
31
 
32
  ## Search rules
33
 
 
48
  - `hf_user_likes(...)` already returns full normalized like rows by default; omit `fields` unless the user asked for a subset.
49
  - When sorting `hf_user_likes(...)` by `repo_likes` or `repo_downloads`, set `ranking_window=50` unless the user explicitly asked for a narrower recent window.
50
  - For human-facing follower/member/liker lists without an explicit requested count, prefer `limit=100` and return coverage when more may exist.
51
+ - For follower/following/member/liker queries that require local filtering on actor fields such as `username` or `fullname`, prefer a bounded scan like `limit=100` / `scan_limit=100` by default, or at most about `200` when a slightly broader sample is justified. Do **not** jump to `1000` unless the user explicitly asked for exhaustive coverage or a very large sample.
52
  - Unknown `fields` / `where` keys now fail fast. Use only canonical field names.
53
+ - Ownership phrasing like "what collections does Qwen have", "collections by Qwen", or "collections owned by Qwen" means an owner lookup, so use `hf_collections_search(owner="Qwen")`, not a keyword-only `query="Qwen"` search; it filters owners case-insensitively.
 
54
  - Ownership phrasing like "what spaces does X have", "what models does X have", or "what datasets does X have" means an author/owner inventory lookup, so use `hf_spaces_search(author="X")`, `hf_models_search(author="X")`, or `hf_datasets_search(author="X")` rather than a global keyword-only search.
55
+ - For profile/detail/social questions about a user or org bio, description, display name, website, GitHub, Twitter/X, LinkedIn, Bluesky, organizations, or pro status — use `hf_profile_summary(...)` first.
56
+ - For join-style questions that need profile details for followers, following, members, likers, or other actor lists, first fetch a **bounded** actor list, filter locally on actor fields like `username` / `fullname`, then hydrate only the bounded matches with `hf_profile_summary(...)`.
57
+ - Do **not** set the initial actor-list limit equal to the whole remaining call budget when each match needs a follow-up profile lookup; reserve budget for the profile-detail calls and return coverage if the hydration step is partial.
58
  - For exact aggregate counts like "how many models/datasets/spaces does X have", prefer `hf_profile_summary(...)['item']` counts. Those overview-owned counts may differ slightly from visible public search/list results, so if the user also asked for the list, preserve that distinction.
59
  - For owner inventory queries without an explicit requested count, use `hf_profile_summary(...)` first when a specific owner is known. If the count is modest, use it to size the follow-up list call; otherwise return a bounded list plus coverage instead of pretending completeness.
60
  - Think like `huggingface_hub`: `search`, `filter`, `author`, repo-type-specific upstream params, then `fields`.
61
  - Push constraints upstream whenever a first-class helper argument exists.
62
  - `post_filter` is only for normalized row filters that cannot be pushed upstream.
63
+ - `num_params` is a first-class upstream model-search arg; use `num_params="min:6B,max:128B"` instead of `post_filter` when possible.
64
+ - For created/updated date constraints, pair local `post_filter` with the matching sort (`created_at` or `last_modified`). Do **not** rely on date-only `post_filter` over an unsorted repo search window.
65
  - Keep `post_filter` simple:
66
  - exact match or `in` for returned fields like `runtime_stage`
67
+ - `gte` / `lte` for normalized numeric fields like `downloads` and `likes`
68
+ - `gte` / `lte` also work for normalized ISO timestamp fields like `created_at` and `last_modified`
69
+ - Do **not** use `post_filter` for things that already have first-class upstream params like `author`, `pipeline_tag`, `num_params` on model search, `dataset_name`, `language`, `models`, or `datasets`.
70
 
71
  Examples:
72
 
73
  ```py
74
+ result = await hf_models_search(pipeline_tag="text-to-image", limit=10)
75
+ result
76
+ ```
77
+
78
+ ```py
79
+ result = await hf_models_search(
80
  pipeline_tag="text-generation",
81
+ num_params="min:20B,max:80B",
82
  sort="trending_score",
83
  limit=50,
 
84
  )
85
+ result
86
+ ```
87
+
88
+ ```py
89
+ result = await hf_collections_search(owner="Qwen", limit=10)
90
+ result
91
  ```
92
 
93
  Field-only pattern:
 
98
  fields=["repo_id", "author", "likes", "downloads", "repo_url"],
99
  limit=3,
100
  )
101
+ result = resp["items"]
102
+ result
103
  ```
104
 
105
  Coverage pattern:
 
112
  limit=20,
113
  fields=["repo_id", "repo_likes", "repo_url"],
114
  )
115
+ result = {"results": resp["items"], "coverage": resp["meta"]}
116
+ result
117
  ```
118
 
119
  Owner-inventory pattern:
 
129
  )
130
  meta = resp.get("meta") or {}
131
  if meta.get("limit_boundary_hit") or meta.get("more_available") not in {False, None}:
132
+ result = {"results": resp["items"], "coverage": {**meta, "profile_spaces_count": count}}
133
+ else:
134
+ result = resp["items"]
135
+ result
136
+ ```
137
+
138
+ Follower-profile join pattern:
139
+
140
+ ```py
141
+ followers_resp = await hf_user_graph(
142
+ relation="followers",
143
+ limit=100,
144
+ scan_limit=100,
145
+ fields=["username", "fullname"],
146
+ )
147
+ followers = followers_resp.get("items") or []
148
+ matches = []
149
+ for follower in followers:
150
+ username = follower.get("username")
151
+ fullname = follower.get("fullname")
152
+ starts_with_b = (
153
+ (isinstance(username, str) and username.lower().startswith("b"))
154
+ or (isinstance(fullname, str) and fullname.lower().startswith("b"))
155
+ )
156
+ if starts_with_b:
157
+ matches.append(follower)
158
+ remaining_profile_calls = max(0, max_calls - 1)
159
+ results = []
160
+ for follower in matches[:remaining_profile_calls]:
161
+ username = follower.get("username")
162
+ if not username:
163
+ continue
164
+ profile = await hf_profile_summary(handle=username)
165
+ item = profile.get("item") or {}
166
+ results.append(
167
+ {
168
+ "username": username,
169
+ "fullname": follower.get("fullname"),
170
+ "github_url": item.get("github_url"),
171
+ }
172
+ )
173
+ result = {
174
+ "results": results,
175
+ "coverage": {
176
+ "followers": followers_resp.get("meta") or {},
177
+ "matching_followers_seen": len(matches),
178
+ "profile_calls_used": len(results),
179
+ "profile_hydration_partial": len(matches) > len(results),
180
+ },
181
+ }
182
+ result
183
  ```
184
 
185
+ Follower-likes aggregation pattern:
186
 
187
  ```py
188
+ followers_resp = await hf_user_graph(relation="followers", limit=100, fields=["username"])
189
+ followers = followers_resp.get("items") or []
190
+ results = []
191
+ for follower in followers:
192
+ username = follower.get("username")
193
+ if not username:
194
+ continue
195
+ likes_resp = await hf_user_likes(
196
+ username=username,
197
+ repo_types=["model"],
198
+ limit=20,
199
+ fields=["repo_id", "liked_at"],
200
+ )
201
+ results.append(
202
+ {
203
+ "follower": username,
204
+ "liked_models": likes_resp.get("items") or [],
205
+ }
206
+ )
207
+ coverage = {
208
+ "followers": followers_resp.get("meta") or {},
209
  }
210
+ result = {"results": results, "coverage": coverage}
211
+ result
212
  ```
213
 
214
+ Current-user pro-follower model-likes pattern:
215
 
216
  ```py
217
+ followers_resp = await hf_user_graph(
218
  relation="followers",
219
  pro_only=True,
220
+ limit=100,
221
  fields=["username"],
222
  )
223
+ followers = followers_resp.get("items") or []
224
+ remaining_calls = max(0, max_calls - 1)
225
+ results = {}
226
+ partial = (
227
+ (followers_resp.get("meta") or {}).get("limit_boundary_hit")
228
+ or (followers_resp.get("meta") or {}).get("more_available") not in {False, None}
229
+ )
230
+ processed_followers = 0
231
+ for follower in followers:
232
+ if remaining_calls <= 0:
233
+ partial = True
234
+ break
235
+ username = follower.get("username")
236
+ if not username:
237
+ continue
238
+ likes_resp = await hf_user_likes(
239
+ username=username,
240
+ repo_types=["model"],
241
+ limit=2,
242
+ fields=["repo_id", "repo_author", "liked_at"],
243
+ )
244
+ remaining_calls -= 1
245
+ likes_meta = likes_resp.get("meta") or {}
246
+ if likes_meta.get("limit_boundary_hit") or likes_meta.get("more_available") not in {False, None}:
247
+ partial = True
248
+ items = likes_resp.get("items") or []
249
+ if items:
250
+ results[username] = items
251
+ processed_followers += 1
252
+ coverage = {
253
+ "followers": followers_resp.get("meta") or {},
254
+ "processed_followers": processed_followers,
255
+ "partial": partial,
256
+ }
257
+ result = {"results": results, "coverage": coverage}
258
+ result
259
  ```
260
 
261
  ## Navigation graph
 
272
  - repo discussions → `hf_repo_discussions(...)`
273
  - specific discussion details → `hf_repo_discussion_details(...)`
274
  - users who liked one repo → `hf_repo_likers(...)`
275
+ - profile / overview / social/detail / aggregate counts → `hf_profile_summary(...)`
276
  - followers / following lists → `hf_user_graph(...)`
277
  - repos a user liked → `hf_user_likes(...)`
278
  - recent activity feed → `hf_recent_activity(...)`
 
304
  - `items` is the canonical list field.
305
  - `item` is just a singleton convenience.
306
  - `meta` contains helper-owned execution, limit, and coverage info.
 
307
 
308
  ## High-signal output rules
309
 
310
  - Prefer compact dict/list outputs over prose when the user asked for fields.
 
311
  - Use canonical snake_case keys in generated code and structured output.
312
  - Use `repo_id` as the display label for repos.
 
 
313
  - For joins/intersections/rankings, fetch the needed working set first and compute locally.
314
  - If the result is partial, use top-level keys `results` and `coverage`.
315
 
.prod/agent-cards/shared/_monty_helper_contracts.md CHANGED
@@ -94,7 +94,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
94
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
95
  - normalized_only: `true`
96
  - limit_contract:
97
- - default_limit: `20`
98
  - max_limit: `5000`
99
  - notes: Thin dataset-search wrapper around the Hub list_datasets path. Prefer this over hf_repo_search for dataset-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
100
 
@@ -108,7 +108,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
108
  - default_fields: `repo_id`, `repo_type`, `author`, `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `repo_url`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
109
  - guaranteed_fields: `repo_id`, `repo_type`, `author`, `repo_url`
110
  - optional_fields: `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
111
- - supported_params: `search`, `filter`, `author`, `apps`, `gated`, `inference`, `inference_provider`, `model_name`, `trained_dataset`, `pipeline_tag`, `emissions_thresholds`, `sort`, `limit`, `expand`, `full`, `card_data`, `fetch_config`, `fields`, `post_filter`
112
  - sort_values: `created_at`, `downloads`, `last_modified`, `likes`, `trending_score`
113
  - expand_values: `author`, `base_models`, `card_data`, `config`, `created_at`, `disabled`, `downloads`, `downloads_all_time`, `eval_results`, `gated`, `gguf`, `inference`, `inference_provider_mapping`, `last_modified`, `library_name`, `likes`, `mask_token`, `model_index`, `pipeline_tag`, `private`, `resource_group`, `safetensors`, `sha`, `siblings`, `spaces`, `tags`, `transformers_info`, `trending_score`, `widget_data`, `xet_enabled`, `gitaly_uid`
114
  - fields_contract:
@@ -119,7 +119,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
119
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
120
  - normalized_only: `true`
121
  - limit_contract:
122
- - default_limit: `20`
123
  - max_limit: `5000`
124
  - notes: Thin model-search wrapper around the Hub list_models path. Prefer this over hf_repo_search for model-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
125
 
@@ -290,7 +290,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
290
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
291
  - normalized_only: `true`
292
  - limit_contract:
293
- - default_limit: `20`
294
  - max_limit: `5000`
295
  - notes: Small generic repo-search helper. Prefer hf_models_search, hf_datasets_search, or hf_spaces_search for single-type queries; use hf_repo_search for intentionally cross-type search. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
296
 
@@ -329,7 +329,7 @@ All helpers return the same envelope: `{ok, item, items, meta, error}`.
329
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
330
  - normalized_only: `true`
331
  - limit_contract:
332
- - default_limit: `20`
333
  - max_limit: `5000`
334
  - notes: Thin space-search wrapper around the Hub list_spaces path. Prefer this over hf_repo_search for space-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
335
 
 
94
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
95
  - normalized_only: `true`
96
  - limit_contract:
97
+ - default_limit: `100`
98
  - max_limit: `5000`
99
  - notes: Thin dataset-search wrapper around the Hub list_datasets path. Prefer this over hf_repo_search for dataset-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
100
 
 
108
  - default_fields: `repo_id`, `repo_type`, `author`, `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `repo_url`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
109
  - guaranteed_fields: `repo_id`, `repo_type`, `author`, `repo_url`
110
  - optional_fields: `likes`, `downloads`, `trending_score`, `created_at`, `last_modified`, `pipeline_tag`, `num_params`, `tags`, `library_name`, `description`, `paperswithcode_id`, `sdk`, `models`, `datasets`, `subdomain`, `runtime_stage`, `runtime`
111
+ - supported_params: `search`, `filter`, `author`, `apps`, `gated`, `inference`, `inference_provider`, `model_name`, `trained_dataset`, `pipeline_tag`, `num_params`, `emissions_thresholds`, `sort`, `limit`, `expand`, `full`, `card_data`, `fetch_config`, `fields`, `post_filter`
112
  - sort_values: `created_at`, `downloads`, `last_modified`, `likes`, `trending_score`
113
  - expand_values: `author`, `base_models`, `card_data`, `config`, `created_at`, `disabled`, `downloads`, `downloads_all_time`, `eval_results`, `gated`, `gguf`, `inference`, `inference_provider_mapping`, `last_modified`, `library_name`, `likes`, `mask_token`, `model_index`, `pipeline_tag`, `private`, `resource_group`, `safetensors`, `sha`, `siblings`, `spaces`, `tags`, `transformers_info`, `trending_score`, `widget_data`, `xet_enabled`, `gitaly_uid`
114
  - fields_contract:
 
119
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
120
  - normalized_only: `true`
121
  - limit_contract:
122
+ - default_limit: `100`
123
  - max_limit: `5000`
124
  - notes: Thin model-search wrapper around the Hub list_models path. Prefer this over hf_repo_search for model-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
125
 
 
290
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
291
  - normalized_only: `true`
292
  - limit_contract:
293
+ - default_limit: `100`
294
  - max_limit: `5000`
295
  - notes: Small generic repo-search helper. Prefer hf_models_search, hf_datasets_search, or hf_spaces_search for single-type queries; use hf_repo_search for intentionally cross-type search. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
296
 
 
329
  - supported_ops: `eq`, `in`, `contains`, `icontains`, `gte`, `lte`
330
  - normalized_only: `true`
331
  - limit_contract:
332
+ - default_limit: `100`
333
  - max_limit: `5000`
334
  - notes: Thin space-search wrapper around the Hub list_spaces path. Prefer this over hf_repo_search for space-only queries. This is a one-shot selective search; if meta.limit_boundary_hit is true, more rows may exist and counts are not exact.
335
 
.prod/agent-cards/shared/_monty_helper_signatures.md CHANGED
@@ -10,9 +10,9 @@ await hf_collections_search(query: 'str | None' = None, owner: 'str | None' = No
10
 
11
  await hf_daily_papers(limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
12
 
13
- await hf_datasets_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, benchmark: 'str | bool | None' = None, dataset_name: 'str | None' = None, gated: 'bool | None' = None, language_creators: 'str | list[str] | None' = None, language: 'str | list[str] | None' = None, multilinguality: 'str | list[str] | None' = None, size_categories: 'str | list[str] | None' = None, task_categories: 'str | list[str] | None' = None, task_ids: 'str | list[str] | None' = None, sort: 'str | None' = None, limit: 'int' = 20, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
14
 
15
- await hf_models_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, apps: 'str | list[str] | None' = None, gated: 'bool | None' = None, inference: 'str | None' = None, inference_provider: 'str | list[str] | None' = None, model_name: 'str | None' = None, trained_dataset: 'str | list[str] | None' = None, pipeline_tag: 'str | None' = None, emissions_thresholds: 'tuple[float, float] | None' = None, sort: 'str | None' = None, limit: 'int' = 20, expand: 'list[str] | None' = None, full: 'bool | None' = None, card_data: 'bool' = False, fetch_config: 'bool' = False, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
16
 
17
  await hf_org_members(organization: 'str', limit: 'int | None' = None, scan_limit: 'int | None' = None, count_only: 'bool' = False, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
18
 
@@ -28,11 +28,11 @@ await hf_repo_discussions(repo_type: 'str', repo_id: 'str', limit: 'int' = 20, f
28
 
29
  await hf_repo_likers(repo_id: 'str', repo_type: 'str', limit: 'int | None' = None, count_only: 'bool' = False, pro_only: 'bool | None' = None, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
30
 
31
- await hf_repo_search(search: 'str | None' = None, repo_type: 'str | None' = None, repo_types: 'list[str] | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, sort: 'str | None' = None, limit: 'int' = 20, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
32
 
33
  await hf_runtime_capabilities(section: 'str | None' = None) -> 'dict[str, Any]'
34
 
35
- await hf_spaces_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, datasets: 'str | list[str] | None' = None, models: 'str | list[str] | None' = None, linked: 'bool' = False, sort: 'str | None' = None, limit: 'int' = 20, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
36
 
37
  await hf_trending(repo_type: 'str' = 'model', limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
38
 
 
10
 
11
  await hf_daily_papers(limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
12
 
13
+ await hf_datasets_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, benchmark: 'str | bool | None' = None, dataset_name: 'str | None' = None, gated: 'bool | None' = None, language_creators: 'str | list[str] | None' = None, language: 'str | list[str] | None' = None, multilinguality: 'str | list[str] | None' = None, size_categories: 'str | list[str] | None' = None, task_categories: 'str | list[str] | None' = None, task_ids: 'str | list[str] | None' = None, sort: 'str | None' = None, limit: 'int' = 100, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
14
 
15
+ await hf_models_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, apps: 'str | list[str] | None' = None, gated: 'bool | None' = None, inference: 'str | None' = None, inference_provider: 'str | list[str] | None' = None, model_name: 'str | None' = None, trained_dataset: 'str | list[str] | None' = None, pipeline_tag: 'str | None' = None, num_params: 'str | None' = None, emissions_thresholds: 'tuple[float, float] | None' = None, sort: 'str | None' = None, limit: 'int' = 100, expand: 'list[str] | None' = None, full: 'bool | None' = None, card_data: 'bool' = False, fetch_config: 'bool' = False, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
16
 
17
  await hf_org_members(organization: 'str', limit: 'int | None' = None, scan_limit: 'int | None' = None, count_only: 'bool' = False, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
18
 
 
28
 
29
  await hf_repo_likers(repo_id: 'str', repo_type: 'str', limit: 'int | None' = None, count_only: 'bool' = False, pro_only: 'bool | None' = None, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
30
 
31
+ await hf_repo_search(search: 'str | None' = None, repo_type: 'str | None' = None, repo_types: 'list[str] | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, sort: 'str | None' = None, limit: 'int' = 100, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
32
 
33
  await hf_runtime_capabilities(section: 'str | None' = None) -> 'dict[str, Any]'
34
 
35
+ await hf_spaces_search(search: 'str | None' = None, filter: 'str | list[str] | None' = None, author: 'str | None' = None, datasets: 'str | list[str] | None' = None, models: 'str | list[str] | None' = None, linked: 'bool' = False, sort: 'str | None' = None, limit: 'int' = 100, expand: 'list[str] | None' = None, full: 'bool | None' = None, fields: 'list[str] | None' = None, post_filter: 'dict[str, Any] | None' = None) -> 'dict[str, Any]'
36
 
37
  await hf_trending(repo_type: 'str' = 'model', limit: 'int' = 20, where: 'dict[str, Any] | None' = None, fields: 'list[str] | None' = None) -> 'dict[str, Any]'
38
 
.prod/monty_api/helpers/repos.py CHANGED
@@ -123,6 +123,9 @@ def _build_repo_search_extra_args(
123
  if value:
124
  normalized["cardData"] = True
125
  continue
 
 
 
126
  if key in {"fetch_config", "linked"}:
127
  if value:
128
  normalized[key] = True
@@ -180,7 +183,7 @@ async def _run_repo_search(
180
  extra_args_by_type: dict[str, dict[str, Any]] | None = None,
181
  ) -> dict[str, Any]:
182
  start_calls = ctx.call_count["n"]
183
- default_limit = ctx._policy_int(helper_name, "default_limit", 20)
184
  max_limit = ctx._policy_int(
185
  helper_name, "max_limit", SELECTIVE_ENDPOINT_RETURN_HARD_CAP
186
  )
@@ -340,9 +343,10 @@ async def hf_models_search(
340
  model_name: str | None = None,
341
  trained_dataset: str | list[str] | None = None,
342
  pipeline_tag: str | None = None,
 
343
  emissions_thresholds: tuple[float, float] | None = None,
344
  sort: str | None = None,
345
- limit: int = 20,
346
  expand: list[str] | None = None,
347
  full: bool | None = None,
348
  card_data: bool = False,
@@ -370,6 +374,7 @@ async def hf_models_search(
370
  "model_name": model_name,
371
  "trained_dataset": trained_dataset,
372
  "pipeline_tag": pipeline_tag,
 
373
  "emissions_thresholds": emissions_thresholds,
374
  "expand": expand,
375
  "full": full,
@@ -395,7 +400,7 @@ async def hf_datasets_search(
395
  task_categories: str | list[str] | None = None,
396
  task_ids: str | list[str] | None = None,
397
  sort: str | None = None,
398
- limit: int = 20,
399
  expand: list[str] | None = None,
400
  full: bool | None = None,
401
  fields: list[str] | None = None,
@@ -439,7 +444,7 @@ async def hf_spaces_search(
439
  models: str | list[str] | None = None,
440
  linked: bool = False,
441
  sort: str | None = None,
442
- limit: int = 20,
443
  expand: list[str] | None = None,
444
  full: bool | None = None,
445
  fields: list[str] | None = None,
@@ -476,7 +481,7 @@ async def hf_repo_search(
476
  filter: str | list[str] | None = None,
477
  author: str | None = None,
478
  sort: str | None = None,
479
- limit: int = 20,
480
  fields: list[str] | None = None,
481
  post_filter: dict[str, Any] | None = None,
482
  ) -> dict[str, Any]:
 
123
  if value:
124
  normalized["cardData"] = True
125
  continue
126
+ if key in {"num_params", "num_parameters"}:
127
+ normalized["num_parameters"] = value
128
+ continue
129
  if key in {"fetch_config", "linked"}:
130
  if value:
131
  normalized[key] = True
 
183
  extra_args_by_type: dict[str, dict[str, Any]] | None = None,
184
  ) -> dict[str, Any]:
185
  start_calls = ctx.call_count["n"]
186
+ default_limit = ctx._policy_int(helper_name, "default_limit", 100)
187
  max_limit = ctx._policy_int(
188
  helper_name, "max_limit", SELECTIVE_ENDPOINT_RETURN_HARD_CAP
189
  )
 
343
  model_name: str | None = None,
344
  trained_dataset: str | list[str] | None = None,
345
  pipeline_tag: str | None = None,
346
+ num_params: str | None = None,
347
  emissions_thresholds: tuple[float, float] | None = None,
348
  sort: str | None = None,
349
+ limit: int = 100,
350
  expand: list[str] | None = None,
351
  full: bool | None = None,
352
  card_data: bool = False,
 
374
  "model_name": model_name,
375
  "trained_dataset": trained_dataset,
376
  "pipeline_tag": pipeline_tag,
377
+ "num_params": num_params,
378
  "emissions_thresholds": emissions_thresholds,
379
  "expand": expand,
380
  "full": full,
 
400
  task_categories: str | list[str] | None = None,
401
  task_ids: str | list[str] | None = None,
402
  sort: str | None = None,
403
+ limit: int = 100,
404
  expand: list[str] | None = None,
405
  full: bool | None = None,
406
  fields: list[str] | None = None,
 
444
  models: str | list[str] | None = None,
445
  linked: bool = False,
446
  sort: str | None = None,
447
+ limit: int = 100,
448
  expand: list[str] | None = None,
449
  full: bool | None = None,
450
  fields: list[str] | None = None,
 
481
  filter: str | list[str] | None = None,
482
  author: str | None = None,
483
  sort: str | None = None,
484
+ limit: int = 100,
485
  fields: list[str] | None = None,
486
  post_filter: dict[str, Any] | None = None,
487
  ) -> dict[str, Any]:
.prod/monty_api/llm_time_hook.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING
5
+
6
+ from fast_agent.constants import FAST_AGENT_TIMING
7
+ from fast_agent.hooks import show_hook_message
8
+ from fast_agent.mcp.helpers.content_helpers import get_text
9
+
10
+ if TYPE_CHECKING:
11
+ from fast_agent.hooks import HookContext
12
+
13
+
14
+ def _timing_payload(ctx: "HookContext") -> dict[str, object] | None:
15
+ channels = ctx.message.channels or {}
16
+ timing_blocks = channels.get(FAST_AGENT_TIMING, [])
17
+ if not timing_blocks:
18
+ return None
19
+
20
+ payload_text = get_text(timing_blocks[0])
21
+ if not payload_text:
22
+ return None
23
+
24
+ try:
25
+ payload = json.loads(payload_text)
26
+ except json.JSONDecodeError:
27
+ return None
28
+
29
+ return payload if isinstance(payload, dict) else None
30
+
31
+
32
+ def _coerce_float(value: object) -> float | None:
33
+ if isinstance(value, bool):
34
+ return None
35
+ if isinstance(value, int | float):
36
+ return float(value)
37
+ return None
38
+
39
+
40
+ def _format_duration_ms(duration_ms: float) -> str:
41
+ if duration_ms >= 1000:
42
+ return f"{duration_ms / 1000:.2f}s"
43
+ return f"{duration_ms:.0f}ms"
44
+
45
+
46
+ async def display_llm_time(ctx: "HookContext") -> None:
47
+ payload = _timing_payload(ctx)
48
+ if payload is None:
49
+ return
50
+
51
+ duration_ms = _coerce_float(payload.get("duration_ms"))
52
+ if duration_ms is None:
53
+ return
54
+
55
+ show_hook_message(
56
+ ctx,
57
+ _format_duration_ms(duration_ms),
58
+ hook_name="llm_time",
59
+ hook_kind="tool",
60
+ )
.prod/monty_api/query_entrypoints.py CHANGED
@@ -21,6 +21,7 @@ from .constants import (
21
  from .runtime_context import build_runtime_helper_environment
22
  from .validation import (
23
  _coerce_jsonish_python_literals,
 
24
  _summarize_limit_hit,
25
  _truncate_result_payload,
26
  _validate_generated_code,
@@ -40,13 +41,28 @@ def _query_debug_enabled() -> bool:
40
  return value.strip().lower() in {"1", "true", "yes", "on"}
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def _log_generated_query(
44
- *, query: str, code: str, max_calls: int | None, timeout_sec: int | None
45
  ) -> None:
46
  if not _query_debug_enabled():
47
  return
48
- print("[monty-debug] query:", file=sys.stderr)
49
- print(query, file=sys.stderr)
 
50
  print("[monty-debug] max_calls:", max_calls, file=sys.stderr)
51
  print("[monty-debug] timeout_sec:", timeout_sec, file=sys.stderr)
52
  print("[monty-debug] code:", file=sys.stderr)
@@ -72,11 +88,17 @@ def _introspect_helper_signatures() -> dict[str, set[str]]:
72
  async def _run_with_monty(
73
  *,
74
  code: str,
75
- query: str,
76
  max_calls: int,
77
  strict_mode: bool,
78
  timeout_sec: int,
79
  ) -> dict[str, Any]:
 
 
 
 
 
 
80
  try:
81
  import pydantic_monty
82
  except Exception as e:
@@ -101,10 +123,24 @@ async def _run_with_monty(
101
  helper_name: str, fn: Callable[..., Any]
102
  ) -> Callable[..., Any]:
103
  async def wrapped(*args: Any, **kwargs: Any) -> Any:
 
 
 
 
 
 
 
104
  result = await fn(*args, **kwargs)
105
  summary = _summarize_limit_hit(helper_name, result)
106
  if summary is not None and len(env.limit_summaries) < 20:
107
  env.limit_summaries.append(summary)
 
 
 
 
 
 
 
108
  return result
109
 
110
  return wrapped
@@ -117,16 +153,19 @@ async def _run_with_monty(
117
  }
118
 
119
  try:
 
120
  result = await pydantic_monty.run_monty_async(
121
  m,
122
- inputs={"query": query, "max_calls": max_calls},
123
  external_functions={
124
  name: _collecting_wrapper(name, fn)
125
  for name, fn in env.helper_functions.items()
126
  },
127
  limits=limits,
128
  )
 
129
  except Exception as e:
 
130
  raise MontyExecutionError(str(e), env.call_count["n"], env.trace) from e
131
 
132
  if env.call_count["n"] == 0:
@@ -200,32 +239,32 @@ async def _run_with_monty(
200
 
201
  def _prepare_query_inputs(
202
  *,
203
- query: str,
204
  code: str,
205
  max_calls: int | None,
206
  timeout_sec: int | None,
207
  ) -> tuple[str, str, int, int]:
208
- if not query or not query.strip():
209
- raise ValueError("query is required")
210
  if not code or not code.strip():
211
  raise ValueError("code is required")
212
 
 
213
  resolved_max_calls = DEFAULT_MAX_CALLS if max_calls is None else max_calls
214
  resolved_timeout_sec = DEFAULT_TIMEOUT_SEC if timeout_sec is None else timeout_sec
215
  normalized_max_calls = max(1, min(int(resolved_max_calls), MAX_CALLS_LIMIT))
216
  normalized_timeout_sec = int(resolved_timeout_sec)
217
  normalized_code = _coerce_jsonish_python_literals(code.strip())
218
  _validate_generated_code(normalized_code)
219
- return query, normalized_code, normalized_max_calls, normalized_timeout_sec
220
 
221
 
222
  async def _execute_query(
223
  *,
224
- query: str,
225
  code: str,
226
  max_calls: int | None,
227
  timeout_sec: int | None,
228
  ) -> dict[str, Any]:
 
229
  prepared_query, prepared_code, prepared_max_calls, prepared_timeout = (
230
  _prepare_query_inputs(
231
  query=query,
@@ -234,6 +273,13 @@ async def _execute_query(
234
  timeout_sec=timeout_sec,
235
  )
236
  )
 
 
 
 
 
 
 
237
  _log_generated_query(
238
  query=prepared_query,
239
  code=prepared_code,
@@ -250,8 +296,8 @@ async def _execute_query(
250
 
251
 
252
  async def hf_hub_query(
253
- query: str,
254
  code: str,
 
255
  max_calls: int | None = DEFAULT_MAX_CALLS,
256
  timeout_sec: int | None = DEFAULT_TIMEOUT_SEC,
257
  ) -> dict[str, Any]:
@@ -270,7 +316,7 @@ async def hf_hub_query(
270
  )
271
  return {
272
  "ok": True,
273
- "data": run["output"],
274
  "error": None,
275
  "api_calls": run["api_calls"],
276
  }
@@ -291,8 +337,8 @@ async def hf_hub_query(
291
 
292
 
293
  async def hf_hub_query_raw(
294
- query: str,
295
  code: str,
 
296
  max_calls: int | None = DEFAULT_MAX_CALLS,
297
  timeout_sec: int | None = DEFAULT_TIMEOUT_SEC,
298
  ) -> Any:
@@ -300,7 +346,7 @@ async def hf_hub_query_raw(
300
 
301
  Best for read-only Hub discovery, lookup, ranking, and relationship
302
  questions when the caller wants a runtime-owned raw envelope:
303
- ``result`` contains the direct ``solve(...)`` output and ``meta`` contains
304
  execution details such as timing, call counts, and limit summaries.
305
  """
306
  started = time.perf_counter()
@@ -313,7 +359,7 @@ async def hf_hub_query_raw(
313
  )
314
  elapsed_ms = int((time.perf_counter() - started) * 1000)
315
  return _wrap_raw_result(
316
- run["output"],
317
  ok=True,
318
  api_calls=run["api_calls"],
319
  elapsed_ms=elapsed_ms,
@@ -341,7 +387,7 @@ async def hf_hub_query_raw(
341
 
342
  def _arg_parser() -> argparse.ArgumentParser:
343
  p = argparse.ArgumentParser(description="Monty-backed API chaining tool (v3)")
344
- p.add_argument("--query", required=True, help="Natural language query")
345
  p.add_argument("--code", default=None, help="Inline Monty code to execute")
346
  p.add_argument(
347
  "--code-file", default=None, help="Path to .py file with Monty code to execute"
@@ -375,8 +421,8 @@ def main() -> int:
375
  try:
376
  out = asyncio.run(
377
  hf_hub_query(
378
- query=args.query,
379
  code=code,
 
380
  max_calls=args.max_calls,
381
  timeout_sec=args.timeout,
382
  )
 
21
  from .runtime_context import build_runtime_helper_environment
22
  from .validation import (
23
  _coerce_jsonish_python_literals,
24
+ _compact_result_metadata,
25
  _summarize_limit_hit,
26
  _truncate_result_payload,
27
  _validate_generated_code,
 
41
  return value.strip().lower() in {"1", "true", "yes", "on"}
42
 
43
 
44
+ def _execution_debug_enabled() -> bool:
45
+ value = os.environ.get("MONTY_DEBUG_EXECUTION", "")
46
+ if value.strip().lower() in {"1", "true", "yes", "on"}:
47
+ return True
48
+ return _query_debug_enabled()
49
+
50
+
51
+ def _debug_log(*parts: Any) -> None:
52
+ if not _execution_debug_enabled():
53
+ return
54
+ print("[monty-debug]", *parts, file=sys.stderr)
55
+ sys.stderr.flush()
56
+
57
+
58
  def _log_generated_query(
59
+ *, query: str | None, code: str, max_calls: int | None, timeout_sec: int | None
60
  ) -> None:
61
  if not _query_debug_enabled():
62
  return
63
+ if query:
64
+ print("[monty-debug] query:", file=sys.stderr)
65
+ print(query, file=sys.stderr)
66
  print("[monty-debug] max_calls:", max_calls, file=sys.stderr)
67
  print("[monty-debug] timeout_sec:", timeout_sec, file=sys.stderr)
68
  print("[monty-debug] code:", file=sys.stderr)
 
88
  async def _run_with_monty(
89
  *,
90
  code: str,
91
+ query: str | None,
92
  max_calls: int,
93
  strict_mode: bool,
94
  timeout_sec: int,
95
  ) -> dict[str, Any]:
96
+ _debug_log(
97
+ "run_monty:start",
98
+ f"max_calls={max_calls}",
99
+ f"timeout_sec={timeout_sec}",
100
+ f"strict_mode={strict_mode}",
101
+ )
102
  try:
103
  import pydantic_monty
104
  except Exception as e:
 
123
  helper_name: str, fn: Callable[..., Any]
124
  ) -> Callable[..., Any]:
125
  async def wrapped(*args: Any, **kwargs: Any) -> Any:
126
+ started = time.perf_counter()
127
+ _debug_log(
128
+ "helper:start",
129
+ helper_name,
130
+ f"args={len(args)}",
131
+ f"kwargs={sorted(kwargs)}",
132
+ )
133
  result = await fn(*args, **kwargs)
134
  summary = _summarize_limit_hit(helper_name, result)
135
  if summary is not None and len(env.limit_summaries) < 20:
136
  env.limit_summaries.append(summary)
137
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
138
+ _debug_log(
139
+ "helper:end",
140
+ helper_name,
141
+ f"elapsed_ms={elapsed_ms}",
142
+ f"api_calls={env.call_count['n']}",
143
+ )
144
  return result
145
 
146
  return wrapped
 
153
  }
154
 
155
  try:
156
+ _debug_log("run_monty:invoke")
157
  result = await pydantic_monty.run_monty_async(
158
  m,
159
+ inputs={"query": query or "", "max_calls": max_calls},
160
  external_functions={
161
  name: _collecting_wrapper(name, fn)
162
  for name, fn in env.helper_functions.items()
163
  },
164
  limits=limits,
165
  )
166
+ _debug_log("run_monty:return", f"api_calls={env.call_count['n']}")
167
  except Exception as e:
168
+ _debug_log("run_monty:error", type(e).__name__, str(e))
169
  raise MontyExecutionError(str(e), env.call_count["n"], env.trace) from e
170
 
171
  if env.call_count["n"] == 0:
 
239
 
240
  def _prepare_query_inputs(
241
  *,
242
+ query: str | None,
243
  code: str,
244
  max_calls: int | None,
245
  timeout_sec: int | None,
246
  ) -> tuple[str, str, int, int]:
 
 
247
  if not code or not code.strip():
248
  raise ValueError("code is required")
249
 
250
+ normalized_query = str(query or "").strip()
251
  resolved_max_calls = DEFAULT_MAX_CALLS if max_calls is None else max_calls
252
  resolved_timeout_sec = DEFAULT_TIMEOUT_SEC if timeout_sec is None else timeout_sec
253
  normalized_max_calls = max(1, min(int(resolved_max_calls), MAX_CALLS_LIMIT))
254
  normalized_timeout_sec = int(resolved_timeout_sec)
255
  normalized_code = _coerce_jsonish_python_literals(code.strip())
256
  _validate_generated_code(normalized_code)
257
+ return normalized_query, normalized_code, normalized_max_calls, normalized_timeout_sec
258
 
259
 
260
  async def _execute_query(
261
  *,
262
+ query: str | None,
263
  code: str,
264
  max_calls: int | None,
265
  timeout_sec: int | None,
266
  ) -> dict[str, Any]:
267
+ _debug_log("execute_query:start")
268
  prepared_query, prepared_code, prepared_max_calls, prepared_timeout = (
269
  _prepare_query_inputs(
270
  query=query,
 
273
  timeout_sec=timeout_sec,
274
  )
275
  )
276
+ _debug_log(
277
+ "execute_query:prepared",
278
+ f"query_len={len(prepared_query)}",
279
+ f"code_len={len(prepared_code)}",
280
+ f"max_calls={prepared_max_calls}",
281
+ f"timeout_sec={prepared_timeout}",
282
+ )
283
  _log_generated_query(
284
  query=prepared_query,
285
  code=prepared_code,
 
296
 
297
 
298
  async def hf_hub_query(
 
299
  code: str,
300
+ query: str | None = None,
301
  max_calls: int | None = DEFAULT_MAX_CALLS,
302
  timeout_sec: int | None = DEFAULT_TIMEOUT_SEC,
303
  ) -> dict[str, Any]:
 
316
  )
317
  return {
318
  "ok": True,
319
+ "data": _compact_result_metadata(run["output"]),
320
  "error": None,
321
  "api_calls": run["api_calls"],
322
  }
 
337
 
338
 
339
  async def hf_hub_query_raw(
 
340
  code: str,
341
+ query: str | None = None,
342
  max_calls: int | None = DEFAULT_MAX_CALLS,
343
  timeout_sec: int | None = DEFAULT_TIMEOUT_SEC,
344
  ) -> Any:
 
346
 
347
  Best for read-only Hub discovery, lookup, ranking, and relationship
348
  questions when the caller wants a runtime-owned raw envelope:
349
+ ``result`` contains the generated script's final `result` value and ``meta`` contains
350
  execution details such as timing, call counts, and limit summaries.
351
  """
352
  started = time.perf_counter()
 
359
  )
360
  elapsed_ms = int((time.perf_counter() - started) * 1000)
361
  return _wrap_raw_result(
362
+ _compact_result_metadata(run["output"]),
363
  ok=True,
364
  api_calls=run["api_calls"],
365
  elapsed_ms=elapsed_ms,
 
387
 
388
  def _arg_parser() -> argparse.ArgumentParser:
389
  p = argparse.ArgumentParser(description="Monty-backed API chaining tool (v3)")
390
+ p.add_argument("--query", default=None, help="Optional natural language query/context")
391
  p.add_argument("--code", default=None, help="Inline Monty code to execute")
392
  p.add_argument(
393
  "--code-file", default=None, help="Path to .py file with Monty code to execute"
 
421
  try:
422
  out = asyncio.run(
423
  hf_hub_query(
 
424
  code=code,
425
+ query=args.query,
426
  max_calls=args.max_calls,
427
  timeout_sec=args.timeout,
428
  )
.prod/monty_api/registry.py CHANGED
@@ -62,6 +62,7 @@ REPO_SEARCH_EXTRA_ARGS: dict[str, set[str]] = {
62
  "inference",
63
  "inference_provider",
64
  "model_name",
 
65
  "pipeline_tag",
66
  "trained_dataset",
67
  },
@@ -350,7 +351,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
350
  default_fields=REPO_SUMMARY_FIELDS,
351
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
352
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
353
- default_limit=20,
354
  max_limit=5_000,
355
  notes=(
356
  "Thin model-search wrapper around the Hub list_models path. Prefer this "
@@ -359,7 +360,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
359
  "are not exact."
360
  ),
361
  ),
362
- pagination={"default_limit": 20, "max_limit": 5_000},
363
  ),
364
  "hf_datasets_search": _config(
365
  "hf_datasets_search",
@@ -368,7 +369,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
368
  default_fields=REPO_SUMMARY_FIELDS,
369
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
370
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
371
- default_limit=20,
372
  max_limit=5_000,
373
  notes=(
374
  "Thin dataset-search wrapper around the Hub list_datasets path. Prefer "
@@ -377,7 +378,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
377
  "and counts are not exact."
378
  ),
379
  ),
380
- pagination={"default_limit": 20, "max_limit": 5_000},
381
  ),
382
  "hf_spaces_search": _config(
383
  "hf_spaces_search",
@@ -386,7 +387,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
386
  default_fields=REPO_SUMMARY_FIELDS,
387
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
388
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
389
- default_limit=20,
390
  max_limit=5_000,
391
  notes=(
392
  "Thin space-search wrapper around the Hub list_spaces path. Prefer this "
@@ -395,7 +396,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
395
  "are not exact."
396
  ),
397
  ),
398
- pagination={"default_limit": 20, "max_limit": 5_000},
399
  ),
400
  "hf_repo_search": _config(
401
  "hf_repo_search",
@@ -404,7 +405,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
404
  default_fields=REPO_SUMMARY_FIELDS,
405
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
406
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
407
- default_limit=20,
408
  max_limit=5_000,
409
  notes=(
410
  "Small generic repo-search helper. Prefer hf_models_search, "
@@ -414,7 +415,7 @@ HELPER_CONFIGS: dict[str, HelperConfig] = {
414
  "and counts are not exact."
415
  ),
416
  ),
417
- pagination={"default_limit": 20, "max_limit": 5_000},
418
  ),
419
  "hf_user_graph": _config(
420
  "hf_user_graph",
 
62
  "inference",
63
  "inference_provider",
64
  "model_name",
65
+ "num_parameters",
66
  "pipeline_tag",
67
  "trained_dataset",
68
  },
 
351
  default_fields=REPO_SUMMARY_FIELDS,
352
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
353
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
354
+ default_limit=100,
355
  max_limit=5_000,
356
  notes=(
357
  "Thin model-search wrapper around the Hub list_models path. Prefer this "
 
360
  "are not exact."
361
  ),
362
  ),
363
+ pagination={"default_limit": 100, "max_limit": 5_000},
364
  ),
365
  "hf_datasets_search": _config(
366
  "hf_datasets_search",
 
369
  default_fields=REPO_SUMMARY_FIELDS,
370
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
371
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
372
+ default_limit=100,
373
  max_limit=5_000,
374
  notes=(
375
  "Thin dataset-search wrapper around the Hub list_datasets path. Prefer "
 
378
  "and counts are not exact."
379
  ),
380
  ),
381
+ pagination={"default_limit": 100, "max_limit": 5_000},
382
  ),
383
  "hf_spaces_search": _config(
384
  "hf_spaces_search",
 
387
  default_fields=REPO_SUMMARY_FIELDS,
388
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
389
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
390
+ default_limit=100,
391
  max_limit=5_000,
392
  notes=(
393
  "Thin space-search wrapper around the Hub list_spaces path. Prefer this "
 
396
  "are not exact."
397
  ),
398
  ),
399
+ pagination={"default_limit": 100, "max_limit": 5_000},
400
  ),
401
  "hf_repo_search": _config(
402
  "hf_repo_search",
 
405
  default_fields=REPO_SUMMARY_FIELDS,
406
  guaranteed_fields=["repo_id", "repo_type", "author", "repo_url"],
407
  optional_fields=REPO_SUMMARY_OPTIONAL_FIELDS,
408
+ default_limit=100,
409
  max_limit=5_000,
410
  notes=(
411
  "Small generic repo-search helper. Prefer hf_models_search, "
 
415
  "and counts are not exact."
416
  ),
417
  ),
418
+ pagination={"default_limit": 100, "max_limit": 5_000},
419
  ),
420
  "hf_user_graph": _config(
421
  "hf_user_graph",
.prod/monty_api/runtime_context.py CHANGED
@@ -1,9 +1,13 @@
1
  from __future__ import annotations
2
 
3
  import os
 
 
4
  from dataclasses import dataclass, field
5
  from typing import TYPE_CHECKING, Any, Callable, NamedTuple, cast
6
 
 
 
7
  from .constants import MAX_CALLS_LIMIT
8
  from .helpers.activity import register_activity_helpers
9
  from .helpers.collections import register_collection_helpers
@@ -82,6 +86,48 @@ class RuntimeHelperEnvironment(NamedTuple):
82
  helper_functions: dict[str, Callable[..., Any]]
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  @dataclass(slots=True)
86
  class RuntimeContext:
87
  max_calls: int
@@ -153,6 +199,8 @@ class RuntimeContext:
153
  json_body: dict[str, Any] | None = None,
154
  ) -> dict[str, Any]:
155
  idx = self._consume_call(endpoint, method)
 
 
156
  try:
157
  resp = call_api_host(
158
  endpoint,
@@ -174,9 +222,29 @@ class RuntimeContext:
174
  method=method,
175
  status=int(resp.get("status") or 0),
176
  )
 
 
 
 
 
 
 
 
 
 
177
  return resp
178
  except Exception as exc:
179
  self._trace_err(idx, endpoint, exc, method=method, status=0)
 
 
 
 
 
 
 
 
 
 
180
  raise
181
 
182
  def _get_hf_api_client(self) -> "HfApi":
@@ -184,24 +252,75 @@ class RuntimeContext:
184
  from huggingface_hub import HfApi
185
 
186
  endpoint = os.getenv("HF_ENDPOINT", "https://huggingface.co").rstrip("/")
 
 
 
187
  self._hf_api_client = HfApi(endpoint=endpoint, token=_load_token())
188
  return self._hf_api_client
189
 
190
  def _host_hf_call(self, endpoint: str, fn: Callable[[], Any]) -> Any:
191
  idx = self._consume_call(endpoint, "GET")
 
 
 
 
 
 
 
 
192
  try:
193
  out = fn()
194
  self._trace_ok(idx, endpoint, method="GET", status=200)
 
 
195
  return out
196
  except Exception as exc:
197
  self._trace_err(idx, endpoint, exc, method="GET", status=0)
 
 
 
 
 
 
 
 
 
198
  raise
199
 
200
  async def call_helper(self, helper_name: str, /, *args: Any, **kwargs: Any) -> Any:
201
  fn = self.helper_registry.get(helper_name)
202
  if not callable(fn):
203
  raise RuntimeError(f"Helper '{helper_name}' is not registered")
204
- return await cast(Callable[..., Any], fn)(*args, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
 
207
  for name, value in {
 
1
  from __future__ import annotations
2
 
3
  import os
4
+ import sys
5
+ import time
6
  from dataclasses import dataclass, field
7
  from typing import TYPE_CHECKING, Any, Callable, NamedTuple, cast
8
 
9
+ import httpx
10
+
11
  from .constants import MAX_CALLS_LIMIT
12
  from .helpers.activity import register_activity_helpers
13
  from .helpers.collections import register_collection_helpers
 
86
  helper_functions: dict[str, Callable[..., Any]]
87
 
88
 
89
+ def _execution_debug_enabled() -> bool:
90
+ for key in ("MONTY_DEBUG_EXECUTION", "MONTY_DEBUG_QUERY"):
91
+ value = os.environ.get(key, "")
92
+ if value.strip().lower() in {"1", "true", "yes", "on"}:
93
+ return True
94
+ return False
95
+
96
+
97
+ def _debug_log(*parts: Any) -> None:
98
+ if not _execution_debug_enabled():
99
+ return
100
+ print("[monty-debug]", *parts, file=sys.stderr)
101
+ sys.stderr.flush()
102
+
103
+
104
+ def _hf_call_timeout_default() -> int:
105
+ raw = os.environ.get("MONTY_HF_CALL_TIMEOUT_SEC", "20").strip()
106
+ try:
107
+ return max(1, int(raw))
108
+ except Exception:
109
+ return 20
110
+
111
+
112
+ _HF_CLIENT_TIMEOUT_SEC: int | None = None
113
+
114
+
115
+ def _configure_hf_client_factory(timeout_sec: int) -> None:
116
+ global _HF_CLIENT_TIMEOUT_SEC
117
+ if _HF_CLIENT_TIMEOUT_SEC == timeout_sec:
118
+ return
119
+ from huggingface_hub.utils._http import hf_request_event_hook, set_client_factory
120
+
121
+ set_client_factory(
122
+ lambda: httpx.Client(
123
+ event_hooks={"request": [hf_request_event_hook]},
124
+ follow_redirects=True,
125
+ timeout=float(timeout_sec),
126
+ )
127
+ )
128
+ _HF_CLIENT_TIMEOUT_SEC = timeout_sec
129
+
130
+
131
  @dataclass(slots=True)
132
  class RuntimeContext:
133
  max_calls: int
 
199
  json_body: dict[str, Any] | None = None,
200
  ) -> dict[str, Any]:
201
  idx = self._consume_call(endpoint, method)
202
+ started = time.perf_counter()
203
+ _debug_log("host_raw:start", f"call={idx}", method, endpoint)
204
  try:
205
  resp = call_api_host(
206
  endpoint,
 
222
  method=method,
223
  status=int(resp.get("status") or 0),
224
  )
225
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
226
+ _debug_log(
227
+ "host_raw:end",
228
+ f"call={idx}",
229
+ method,
230
+ endpoint,
231
+ f"ok={bool(resp.get('ok'))}",
232
+ f"status={resp.get('status')}",
233
+ f"elapsed_ms={elapsed_ms}",
234
+ )
235
  return resp
236
  except Exception as exc:
237
  self._trace_err(idx, endpoint, exc, method=method, status=0)
238
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
239
+ _debug_log(
240
+ "host_raw:error",
241
+ f"call={idx}",
242
+ method,
243
+ endpoint,
244
+ type(exc).__name__,
245
+ str(exc),
246
+ f"elapsed_ms={elapsed_ms}",
247
+ )
248
  raise
249
 
250
  def _get_hf_api_client(self) -> "HfApi":
 
252
  from huggingface_hub import HfApi
253
 
254
  endpoint = os.getenv("HF_ENDPOINT", "https://huggingface.co").rstrip("/")
255
+ _configure_hf_client_factory(
256
+ max(1, min(self.timeout_sec, _hf_call_timeout_default()))
257
+ )
258
  self._hf_api_client = HfApi(endpoint=endpoint, token=_load_token())
259
  return self._hf_api_client
260
 
261
  def _host_hf_call(self, endpoint: str, fn: Callable[[], Any]) -> Any:
262
  idx = self._consume_call(endpoint, "GET")
263
+ started = time.perf_counter()
264
+ timeout_sec = max(1, min(self.timeout_sec, _hf_call_timeout_default()))
265
+ _debug_log(
266
+ "host_hf:start",
267
+ f"call={idx}",
268
+ endpoint,
269
+ f"timeout_sec={timeout_sec}",
270
+ )
271
  try:
272
  out = fn()
273
  self._trace_ok(idx, endpoint, method="GET", status=200)
274
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
275
+ _debug_log("host_hf:end", f"call={idx}", endpoint, f"elapsed_ms={elapsed_ms}")
276
  return out
277
  except Exception as exc:
278
  self._trace_err(idx, endpoint, exc, method="GET", status=0)
279
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
280
+ _debug_log(
281
+ "host_hf:error",
282
+ f"call={idx}",
283
+ endpoint,
284
+ type(exc).__name__,
285
+ str(exc),
286
+ f"elapsed_ms={elapsed_ms}",
287
+ )
288
  raise
289
 
290
  async def call_helper(self, helper_name: str, /, *args: Any, **kwargs: Any) -> Any:
291
  fn = self.helper_registry.get(helper_name)
292
  if not callable(fn):
293
  raise RuntimeError(f"Helper '{helper_name}' is not registered")
294
+ started = time.perf_counter()
295
+ _debug_log(
296
+ "runtime_helper:start",
297
+ helper_name,
298
+ f"args={len(args)}",
299
+ f"kwargs={sorted(kwargs)}",
300
+ f"budget_remaining={self._budget_remaining()}",
301
+ )
302
+ try:
303
+ result = await cast(Callable[..., Any], fn)(*args, **kwargs)
304
+ except Exception as exc:
305
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
306
+ _debug_log(
307
+ "runtime_helper:error",
308
+ helper_name,
309
+ type(exc).__name__,
310
+ str(exc),
311
+ f"elapsed_ms={elapsed_ms}",
312
+ )
313
+ raise
314
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
315
+ ok = result.get("ok") if isinstance(result, dict) else None
316
+ _debug_log(
317
+ "runtime_helper:end",
318
+ helper_name,
319
+ f"ok={ok}",
320
+ f"elapsed_ms={elapsed_ms}",
321
+ f"budget_remaining={self._budget_remaining()}",
322
+ )
323
+ return result
324
 
325
 
326
  for name, value in {
.prod/monty_api/runtime_filtering.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  from typing import Any
4
 
5
  from .constants import (
@@ -16,6 +17,23 @@ from .constants import (
16
  from .http_runtime import _as_int
17
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def _allowed_field_set(allowed_fields: tuple[str, ...] | list[str] | set[str]) -> set[str]:
20
  return {str(field).strip() for field in allowed_fields if str(field).strip()}
21
 
@@ -172,13 +190,25 @@ def _item_matches_where(
172
  if "gte" in cond:
173
  left = _as_int(value)
174
  right = _as_int(cond.get("gte"))
175
- if left is None or right is None or left < right:
176
- return False
 
 
 
 
 
 
177
  if "lte" in cond:
178
  left = _as_int(value)
179
  right = _as_int(cond.get("lte"))
180
- if left is None or right is None or left > right:
181
- return False
 
 
 
 
 
 
182
  continue
183
  if isinstance(cond, (list, tuple, set)):
184
  if value not in cond:
 
1
  from __future__ import annotations
2
 
3
+ from datetime import UTC, datetime
4
  from typing import Any
5
 
6
  from .constants import (
 
17
  from .http_runtime import _as_int
18
 
19
 
20
+ def _as_datetime(value: Any) -> datetime | None:
21
+ if not isinstance(value, str):
22
+ return None
23
+ text = value.strip()
24
+ if not text:
25
+ return None
26
+ if text.endswith("Z"):
27
+ text = f"{text[:-1]}+00:00"
28
+ try:
29
+ parsed = datetime.fromisoformat(text)
30
+ except Exception:
31
+ return None
32
+ if parsed.tzinfo is None:
33
+ return parsed.replace(tzinfo=UTC)
34
+ return parsed
35
+
36
+
37
  def _allowed_field_set(allowed_fields: tuple[str, ...] | list[str] | set[str]) -> set[str]:
38
  return {str(field).strip() for field in allowed_fields if str(field).strip()}
39
 
 
190
  if "gte" in cond:
191
  left = _as_int(value)
192
  right = _as_int(cond.get("gte"))
193
+ if left is not None and right is not None:
194
+ if left < right:
195
+ return False
196
+ else:
197
+ left_dt = _as_datetime(value)
198
+ right_dt = _as_datetime(cond.get("gte"))
199
+ if left_dt is None or right_dt is None or left_dt < right_dt:
200
+ return False
201
  if "lte" in cond:
202
  left = _as_int(value)
203
  right = _as_int(cond.get("lte"))
204
+ if left is not None and right is not None:
205
+ if left > right:
206
+ return False
207
+ else:
208
+ left_dt = _as_datetime(value)
209
+ right_dt = _as_datetime(cond.get("lte"))
210
+ if left_dt is None or right_dt is None or left_dt > right_dt:
211
+ return False
212
  continue
213
  if isinstance(cond, (list, tuple, set)):
214
  if value not in cond:
.prod/monty_api/tool_entrypoints.py CHANGED
@@ -23,28 +23,28 @@ from monty_api import ( # noqa: E402
23
 
24
 
25
  async def hf_hub_query(
26
- query: str,
27
  code: str,
 
28
  max_calls: int | None = None,
29
  timeout_sec: int | None = None,
30
  ) -> dict[str, Any]:
31
  return await _hf_hub_query(
32
- query=query,
33
  code=code,
 
34
  max_calls=max_calls,
35
  timeout_sec=timeout_sec,
36
  )
37
 
38
 
39
  async def hf_hub_query_raw(
40
- query: str,
41
  code: str,
 
42
  max_calls: int | None = None,
43
  timeout_sec: int | None = None,
44
  ) -> Any:
45
  return await _hf_hub_query_raw(
46
- query=query,
47
  code=code,
 
48
  max_calls=max_calls,
49
  timeout_sec=timeout_sec,
50
  )
 
23
 
24
 
25
  async def hf_hub_query(
 
26
  code: str,
27
+ query: str | None = None,
28
  max_calls: int | None = None,
29
  timeout_sec: int | None = None,
30
  ) -> dict[str, Any]:
31
  return await _hf_hub_query(
 
32
  code=code,
33
+ query=query,
34
  max_calls=max_calls,
35
  timeout_sec=timeout_sec,
36
  )
37
 
38
 
39
  async def hf_hub_query_raw(
 
40
  code: str,
41
+ query: str | None = None,
42
  max_calls: int | None = None,
43
  timeout_sec: int | None = None,
44
  ) -> Any:
45
  return await _hf_hub_query_raw(
 
46
  code=code,
47
+ query=query,
48
  max_calls=max_calls,
49
  timeout_sec=timeout_sec,
50
  )
.prod/monty_api/validation.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import ast
 
4
  import re
5
  import tokenize
6
  from io import StringIO
@@ -119,6 +120,87 @@ def _truncate_result_payload(output: Any) -> Any:
119
  return trimmed
120
 
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  def _is_helper_envelope(output: Any) -> bool:
123
  return (
124
  isinstance(output, dict)
@@ -139,8 +221,7 @@ def _summarize_limit_hit(helper_name: str, result: Any) -> dict[str, Any] | None
139
  truncated_by = str(meta.get("truncated_by") or "")
140
  limit_hit = any(
141
  [
142
- meta.get("truncated") is True,
143
- meta.get("hard_cap_applied") is True,
144
  truncated_by in {"scan_limit", "page_limit", "multiple"},
145
  ]
146
  )
@@ -233,54 +314,27 @@ def _validate_generated_code(code: str) -> None:
233
 
234
  if not isinstance(parsed, ast.Module):
235
  raise ValueError("Generated code must be a Python module")
236
-
237
- solve_defs = [
238
- node
239
- for node in parsed.body
240
- if isinstance(node, ast.AsyncFunctionDef) and node.name == "solve"
241
- ]
242
- if not solve_defs:
243
- raise ValueError(
244
- "Generated code must define `async def solve(query, max_calls): ...`."
245
- )
246
-
247
- def _valid_solve_signature(node: ast.AsyncFunctionDef) -> bool:
248
- args = node.args
249
- return (
250
- not args.posonlyargs
251
- and len(args.args) == 2
252
- and [arg.arg for arg in args.args] == ["query", "max_calls"]
253
- and args.vararg is None
254
- and not args.kwonlyargs
255
- and args.kwarg is None
256
- and not args.defaults
257
- and not args.kw_defaults
258
- )
259
-
260
- if not any(_valid_solve_signature(node) for node in solve_defs):
261
- raise ValueError(
262
- "`solve` must have signature `async def solve(query, max_calls): ...`."
263
- )
264
-
265
  if not parsed.body:
266
  raise ValueError("Generated code is empty")
267
 
268
  final_stmt = parsed.body[-1]
269
- valid_final_await = (
270
  isinstance(final_stmt, ast.Expr)
271
- and isinstance(final_stmt.value, ast.Await)
272
- and isinstance(final_stmt.value.value, ast.Call)
273
- and isinstance(final_stmt.value.value.func, ast.Name)
274
- and final_stmt.value.value.func.id == "solve"
275
- and len(final_stmt.value.value.args) == 2
276
- and not final_stmt.value.value.keywords
277
- and all(isinstance(arg, ast.Name) for arg in final_stmt.value.value.args)
278
- and [cast(ast.Name, arg).id for arg in final_stmt.value.value.args]
279
- == ["query", "max_calls"]
 
 
280
  )
281
- if not valid_final_await:
282
  raise ValueError(
283
- "Generated code must end with `await solve(query, max_calls)`."
284
  )
285
 
286
  for node in ast.walk(parsed):
 
1
  from __future__ import annotations
2
 
3
  import ast
4
+ import os
5
  import re
6
  import tokenize
7
  from io import StringIO
 
120
  return trimmed
121
 
122
 
123
+ def _verbose_result_meta_enabled() -> bool:
124
+ value = os.environ.get("MONTY_VERBOSE_RESULT_META", "")
125
+ return value.strip().lower() in {"1", "true", "yes", "on"}
126
+
127
+
128
+ def _is_helper_meta_dict(value: Any) -> bool:
129
+ return (
130
+ isinstance(value, dict)
131
+ and isinstance(value.get("source"), str)
132
+ and (
133
+ value.get("normalized") is True
134
+ or "budget_used" in value
135
+ or "budget_remaining" in value
136
+ )
137
+ )
138
+
139
+
140
+ def _helper_meta_is_partial(value: dict[str, Any]) -> bool:
141
+ return any(
142
+ [
143
+ value.get("truncated") is True,
144
+ value.get("more_available") not in {False, None},
145
+ value.get("limit_boundary_hit") is True,
146
+ value.get("sample_complete") is False,
147
+ value.get("exact_count") is False,
148
+ value.get("ranking_complete") is False,
149
+ value.get("ranking_window_hit") is True,
150
+ value.get("hard_cap_applied") is True,
151
+ ]
152
+ )
153
+
154
+
155
+ def _compact_helper_meta(value: dict[str, Any]) -> dict[str, Any]:
156
+ partial = _helper_meta_is_partial(value)
157
+ compact: dict[str, Any] = {
158
+ "partial": partial,
159
+ }
160
+ for key in (
161
+ "source",
162
+ "returned",
163
+ "total",
164
+ "matched",
165
+ "more_available",
166
+ "truncated",
167
+ "truncated_by",
168
+ "exact_count",
169
+ "sample_complete",
170
+ "hard_cap_applied",
171
+ "limit_boundary_hit",
172
+ "can_request_more",
173
+ "next_request_hint",
174
+ "ranking_window",
175
+ "ranking_window_hit",
176
+ "ranking_complete",
177
+ "ranking_next_request_hint",
178
+ "relation",
179
+ "username",
180
+ "organization",
181
+ "entity",
182
+ "entity_type",
183
+ "handle",
184
+ ):
185
+ if value.get(key) is not None:
186
+ compact[key] = value.get(key)
187
+ if compact.get("total") is None and value.get("total_available") is not None:
188
+ compact["total"] = value.get("total_available")
189
+ return compact
190
+
191
+
192
+ def _compact_result_metadata(value: Any) -> Any:
193
+ if _verbose_result_meta_enabled():
194
+ return value
195
+ if _is_helper_meta_dict(value):
196
+ return _compact_helper_meta(value)
197
+ if isinstance(value, dict):
198
+ return {key: _compact_result_metadata(item) for key, item in value.items()}
199
+ if isinstance(value, list):
200
+ return [_compact_result_metadata(item) for item in value]
201
+ return value
202
+
203
+
204
  def _is_helper_envelope(output: Any) -> bool:
205
  return (
206
  isinstance(output, dict)
 
221
  truncated_by = str(meta.get("truncated_by") or "")
222
  limit_hit = any(
223
  [
224
+ _helper_meta_is_partial(meta),
 
225
  truncated_by in {"scan_limit", "page_limit", "multiple"},
226
  ]
227
  )
 
314
 
315
  if not isinstance(parsed, ast.Module):
316
  raise ValueError("Generated code must be a Python module")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  if not parsed.body:
318
  raise ValueError("Generated code is empty")
319
 
320
  final_stmt = parsed.body[-1]
321
+ final_is_result = (
322
  isinstance(final_stmt, ast.Expr)
323
+ and isinstance(final_stmt.value, ast.Name)
324
+ and final_stmt.value.id == "result"
325
+ )
326
+ if not final_is_result:
327
+ raise ValueError(
328
+ "Generated code must assign the final output to `result` and end with a final line containing only `result` (do not stop after `result = ...`)."
329
+ )
330
+
331
+ has_result_assignment = any(
332
+ isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id == "result"
333
+ for node in ast.walk(parsed)
334
  )
335
+ if not has_result_assignment:
336
  raise ValueError(
337
+ "Generated code must assign the final output to `result` before the final `result` line."
338
  )
339
 
340
  for node in ast.walk(parsed):
Dockerfile CHANGED
@@ -17,7 +17,7 @@ RUN uv pip install --system --no-cache \
17
  "fast-agent-mcp==0.6.1" \
18
  /tmp/wheels/prefab_ui-0.13.2.dev5+a585463-py3-none-any.whl \
19
  huggingface_hub \
20
- "pydantic-monty==0.0.8"
21
 
22
  COPY --link ./ /app
23
  RUN chown -R 1000:1000 /app
 
17
  "fast-agent-mcp==0.6.1" \
18
  /tmp/wheels/prefab_ui-0.13.2.dev5+a585463-py3-none-any.whl \
19
  huggingface_hub \
20
+ "pydantic-monty==0.0.17"
21
 
22
  COPY --link ./ /app
23
  RUN chown -R 1000:1000 /app
scripts/prefab_hub_ui.py CHANGED
@@ -778,6 +778,55 @@ def _build_user_profile_card(title: str, values: dict[str, Any]) -> dict[str, An
778
  }
779
 
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  def _prefers_wide_layout(value: Any) -> bool:
782
  if isinstance(value, list):
783
  return bool(value) and all(isinstance(item, dict) for item in value)
@@ -1053,6 +1102,10 @@ def _render_list(
1053
 
1054
  if all(isinstance(item, dict) for item in value):
1055
  rows = [item for item in value if isinstance(item, dict)]
 
 
 
 
1056
  table_card = _build_table_card(title, rows, description=description)
1057
  distribution_fields = _select_distribution_fields(rows)
1058
  if distribution_fields is None:
 
778
  }
779
 
780
 
781
+ def _looks_like_user_list(rows: list[dict[str, Any]]) -> bool:
782
+ return bool(rows) and all(_looks_like_user_profile(row) for row in rows)
783
+
784
+
785
+ def _build_user_grid_card(
786
+ title: str,
787
+ rows: list[dict[str, Any]],
788
+ *,
789
+ description: str | None = None,
790
+ ) -> dict[str, Any] | None:
791
+ cards = [
792
+ card
793
+ for row in rows
794
+ if isinstance(row, dict)
795
+ for card in [_build_user_profile_card(title, row)]
796
+ if card is not None
797
+ ]
798
+ if not cards:
799
+ return None
800
+
801
+ return {
802
+ "type": "Card",
803
+ "children": [
804
+ {
805
+ "type": "CardHeader",
806
+ "children": [
807
+ {"type": "CardTitle", "content": title},
808
+ *(
809
+ [{"type": "CardDescription", "content": description}]
810
+ if description
811
+ else []
812
+ ),
813
+ ],
814
+ },
815
+ {
816
+ "type": "CardContent",
817
+ "children": [
818
+ {
819
+ "type": "Grid",
820
+ "gap": 4,
821
+ "minColumnWidth": "18rem",
822
+ "children": cards,
823
+ }
824
+ ],
825
+ },
826
+ ],
827
+ }
828
+
829
+
830
  def _prefers_wide_layout(value: Any) -> bool:
831
  if isinstance(value, list):
832
  return bool(value) and all(isinstance(item, dict) for item in value)
 
1102
 
1103
  if all(isinstance(item, dict) for item in value):
1104
  rows = [item for item in value if isinstance(item, dict)]
1105
+ if _looks_like_user_list(rows):
1106
+ user_grid = _build_user_grid_card(title, rows, description=description)
1107
+ if user_grid is not None:
1108
+ return [user_grid]
1109
  table_card = _build_table_card(title, rows, description=description)
1110
  distribution_fields = _select_distribution_fields(rows)
1111
  if distribution_fields is None: