Deploy gen-ui with pydantic-monty 0.0.17
Browse files- .prefab/tool-cards/monty_api_tool_v2.py +4 -4
- .prod/agent-cards/shared/_monty_codegen_shared.md +169 -51
- .prod/agent-cards/shared/_monty_codegen_shared.template.md +160 -42
- .prod/agent-cards/shared/_monty_helper_contracts.md +5 -5
- .prod/agent-cards/shared/_monty_helper_signatures.md +4 -4
- .prod/monty_api/helpers/repos.py +10 -5
- .prod/monty_api/llm_time_hook.py +60 -0
- .prod/monty_api/query_entrypoints.py +63 -17
- .prod/monty_api/registry.py +9 -8
- .prod/monty_api/runtime_context.py +120 -1
- .prod/monty_api/runtime_filtering.py +34 -4
- .prod/monty_api/tool_entrypoints.py +4 -4
- .prod/monty_api/validation.py +97 -43
- Dockerfile +1 -1
- scripts/prefab_hub_ui.py +53 -0
.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 |
-
|
| 22 |
query: str,
|
| 23 |
code: str,
|
| 24 |
max_calls: int | None = None,
|
| 25 |
timeout_sec: int | None = None,
|
| 26 |
) -> dict[str, Any]:
|
| 27 |
-
return
|
| 28 |
query=query,
|
| 29 |
code=code,
|
| 30 |
max_calls=max_calls,
|
|
@@ -32,13 +32,13 @@ async def hf_hub_query(
|
|
| 32 |
)
|
| 33 |
|
| 34 |
|
| 35 |
-
|
| 36 |
query: str,
|
| 37 |
code: str,
|
| 38 |
max_calls: int | None = None,
|
| 39 |
timeout_sec: int | None = None,
|
| 40 |
) -> Any:
|
| 41 |
-
return
|
| 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 |
-
-
|
| 7 |
|
| 8 |
```py
|
| 9 |
-
|
| 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 |
-
-
|
| 18 |
-
- Do **not** hand-build JSON strings
|
| 19 |
-
-
|
| 20 |
-
- If the
|
| 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 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
-
|
|
|
|
|
|
|
| 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 `
|
| 57 |
-
- `
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
```
|
| 115 |
|
| 116 |
-
|
| 117 |
|
| 118 |
```py
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
|
|
|
|
|
|
| 125 |
```
|
| 126 |
|
| 127 |
-
|
| 128 |
|
| 129 |
```py
|
| 130 |
-
|
| 131 |
relation="followers",
|
| 132 |
pro_only=True,
|
| 133 |
-
limit=
|
| 134 |
fields=["username"],
|
| 135 |
)
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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' =
|
| 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' =
|
| 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' =
|
| 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' =
|
| 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: `
|
| 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: `
|
| 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: `
|
| 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: `
|
| 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 |
-
-
|
| 7 |
|
| 8 |
```py
|
| 9 |
-
|
| 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 |
-
-
|
| 18 |
-
- Do **not** hand-build JSON strings
|
| 19 |
-
-
|
| 20 |
-
- If the
|
| 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 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
-
|
|
|
|
|
|
|
| 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 `
|
| 57 |
-
- `
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
```
|
| 115 |
|
| 116 |
-
|
| 117 |
|
| 118 |
```py
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
|
|
|
|
|
|
| 125 |
```
|
| 126 |
|
| 127 |
-
|
| 128 |
|
| 129 |
```py
|
| 130 |
-
|
| 131 |
relation="followers",
|
| 132 |
pro_only=True,
|
| 133 |
-
limit=
|
| 134 |
fields=["username"],
|
| 135 |
)
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: `
|
| 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: `
|
| 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: `
|
| 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: `
|
| 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' =
|
| 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' =
|
| 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' =
|
| 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' =
|
| 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",
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 |
-
|
| 49 |
-
|
|
|
|
| 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
|
| 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
|
| 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",
|
| 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=
|
| 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":
|
| 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=
|
| 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":
|
| 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=
|
| 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":
|
| 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=
|
| 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":
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
if "lte" in cond:
|
| 178 |
left = _as_int(value)
|
| 179 |
right = _as_int(cond.get("lte"))
|
| 180 |
-
if left is None
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 270 |
isinstance(final_stmt, ast.Expr)
|
| 271 |
-
and isinstance(final_stmt.value, ast.
|
| 272 |
-
and
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
| 280 |
)
|
| 281 |
-
if not
|
| 282 |
raise ValueError(
|
| 283 |
-
"Generated code must
|
| 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.
|
| 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:
|